mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
237 Commits
feat/prici
...
fix-ios-sy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15c27ed273 | ||
|
|
97a75d9498 | ||
|
|
950028e3b4 | ||
|
|
874726c712 | ||
|
|
0f380b9962 | ||
|
|
2ce569726c | ||
|
|
a38fe7da43 | ||
|
|
057b563608 | ||
|
|
3741e26219 | ||
|
|
86bf8f68bf | ||
|
|
e7f67d1bb9 | ||
|
|
9c28e6bb93 | ||
|
|
0f84fab810 | ||
|
|
228a8e612f | ||
|
|
175c6cdba0 | ||
|
|
d3e2fcc3ac | ||
|
|
dafc2de2b3 | ||
|
|
f9eb9a1ad9 | ||
|
|
1f5001210b | ||
|
|
3150eafb89 | ||
|
|
ad1e3f2c67 | ||
|
|
d67b78d86e | ||
|
|
25f74ff04d | ||
|
|
be7c1453fe | ||
|
|
daa60939c3 | ||
|
|
ac659b05e4 | ||
|
|
8391d51e02 | ||
|
|
ad78b3a931 | ||
|
|
e3a1fdd87d | ||
|
|
12f24db3af | ||
|
|
2af805017c | ||
|
|
97d4ca0afe | ||
|
|
8b5c246531 | ||
|
|
8f2113b155 | ||
|
|
a378bd397b | ||
|
|
a315473ea4 | ||
|
|
7dc581b9f3 | ||
|
|
5ef392ca31 | ||
|
|
846c949cd1 | ||
|
|
d88ead4609 | ||
|
|
055c109563 | ||
|
|
378a7197c2 | ||
|
|
89ee0f8d27 | ||
|
|
c9ea9b1d4e | ||
|
|
a91532839f | ||
|
|
b32f6b2460 | ||
|
|
e02970b936 | ||
|
|
1f24fd4758 | ||
|
|
5a16403a13 | ||
|
|
48bbaad1b8 | ||
|
|
520711eec9 | ||
|
|
3f82a4ebd4 | ||
|
|
4feb22fc12 | ||
|
|
4e0e8dbb66 | ||
|
|
431c4eae14 | ||
|
|
6373cbc9a4 | ||
|
|
bbf3477512 | ||
|
|
ca30ac86d7 | ||
|
|
88aecb3af2 | ||
|
|
c5a78b0293 | ||
|
|
b0bc1ec4a6 | ||
|
|
84b3892053 | ||
|
|
d985683c0f | ||
|
|
008ce9222e | ||
|
|
c360fa8c9b | ||
|
|
c1afd4692d | ||
|
|
7748cf453c | ||
|
|
34d0123351 | ||
|
|
af1cfc92c2 | ||
|
|
59e06ef938 | ||
|
|
9d12ef82c0 | ||
|
|
0a9bfd4b69 | ||
|
|
329248ebbd | ||
|
|
cd51f2f2c9 | ||
|
|
a3e7d5bd84 | ||
|
|
6a261d284f | ||
|
|
eddf9f6d04 | ||
|
|
33e0f81988 | ||
|
|
0df8f80b48 | ||
|
|
2f4590fbb6 | ||
|
|
a011857286 | ||
|
|
cc9e7e10b6 | ||
|
|
78e04bfac5 | ||
|
|
652231d5fd | ||
|
|
343920d376 | ||
|
|
1f3e7574f5 | ||
|
|
ea9b10ff23 | ||
|
|
232a347f4a | ||
|
|
00053cb99d | ||
|
|
03a2a2f9cc | ||
|
|
09c8c66841 | ||
|
|
53e4bda69d | ||
|
|
4c77c8e22c | ||
|
|
9afb4a52c2 | ||
|
|
b3d6fd7f5b | ||
|
|
245d022f58 | ||
|
|
2d226db271 | ||
|
|
9281f9a390 | ||
|
|
d30bb56e75 | ||
|
|
eb92989478 | ||
|
|
d0dcdcd337 | ||
|
|
60aa92ca03 | ||
|
|
e2bbc1425b | ||
|
|
fc01103353 | ||
|
|
22919561fe | ||
|
|
745cbc082f | ||
|
|
6671a97181 | ||
|
|
b7aaa382b1 | ||
|
|
ecfa0e2c6b | ||
|
|
58c6cce4e4 | ||
|
|
b388a550a5 | ||
|
|
f0c5f6b949 | ||
|
|
459cd70c32 | ||
|
|
ece142e8d8 | ||
|
|
5f1f308540 | ||
|
|
7437b29518 | ||
|
|
7acb0a8c0f | ||
|
|
a1a24ee865 | ||
|
|
02008ad098 | ||
|
|
0a02431f6a | ||
|
|
9291711036 | ||
|
|
f3e0fda5fa | ||
|
|
0e721a4948 | ||
|
|
e0a704d022 | ||
|
|
e36bc816ea | ||
|
|
f8a62ca437 | ||
|
|
22ecb079b0 | ||
|
|
06333cf9aa | ||
|
|
1ad199ce53 | ||
|
|
ceebda5314 | ||
|
|
e2d33381cc | ||
|
|
2417f8e233 | ||
|
|
a240b0b3d7 | ||
|
|
fab085579a | ||
|
|
a5faa4acea | ||
|
|
4014f2a5b5 | ||
|
|
fb62839dad | ||
|
|
7bf8f1cfc1 | ||
|
|
b8a727eee8 | ||
|
|
52e12de296 | ||
|
|
0bb2345ccb | ||
|
|
78e957eed4 | ||
|
|
865efaa860 | ||
|
|
2931fd0b3e | ||
|
|
faae68928b | ||
|
|
21ada0e2d2 | ||
|
|
2337b8cd85 | ||
|
|
52c8d0b86c | ||
|
|
ce98cea4e7 | ||
|
|
b66a093cc2 | ||
|
|
a3e5ae4cf5 | ||
|
|
cddd08c32d | ||
|
|
5c3ad96cbe | ||
|
|
55b441d14e | ||
|
|
f740a211ce | ||
|
|
3cf00cb0f5 | ||
|
|
5659576ce3 | ||
|
|
55842d44ea | ||
|
|
43238692e4 | ||
|
|
fa1f64f505 | ||
|
|
25e3d56435 | ||
|
|
8722524c98 | ||
|
|
c9b1b42461 | ||
|
|
fa54f7e7f7 | ||
|
|
26d1de0620 | ||
|
|
b21548780a | ||
|
|
c8ba9266a7 | ||
|
|
9e9ef15737 | ||
|
|
4d7d2fbf31 | ||
|
|
b7ddae6950 | ||
|
|
1d60f2132d | ||
|
|
e9e4a76ac2 | ||
|
|
4d3a2cc574 | ||
|
|
c516c8a0ba | ||
|
|
24d5a0746b | ||
|
|
dee6c86d15 | ||
|
|
33e64416f3 | ||
|
|
a74637e9c0 | ||
|
|
ccd1b6d69b | ||
|
|
3750dd38dc | ||
|
|
1506cc257d | ||
|
|
fe7c7889e8 | ||
|
|
3f54eee11d | ||
|
|
11b5922c2c | ||
|
|
71b85d2a4d | ||
|
|
289832d166 | ||
|
|
a88e612c35 | ||
|
|
bda456362b | ||
|
|
f6f644c55d | ||
|
|
08c92333d6 | ||
|
|
4d72b77f29 | ||
|
|
4f9d06737f | ||
|
|
bfc494a3d4 | ||
|
|
e3b004d35f | ||
|
|
bd05ad6548 | ||
|
|
ffe068dbd5 | ||
|
|
b1f21c1abe | ||
|
|
b494efb8ce | ||
|
|
99ec45ec2a | ||
|
|
9cea027506 | ||
|
|
8bfc158c51 | ||
|
|
d6ed99282d | ||
|
|
ac92f3685f | ||
|
|
05fea49baf | ||
|
|
ed16143a2c | ||
|
|
6618a21a63 | ||
|
|
5b472748a1 | ||
|
|
a117aab558 | ||
|
|
ad2ab03be7 | ||
|
|
514112c077 | ||
|
|
6dd939af5b | ||
|
|
82f9c907be | ||
|
|
3cd6c6a371 | ||
|
|
8437394891 | ||
|
|
29cb444746 | ||
|
|
41eb22b5ca | ||
|
|
8577c32c1f | ||
|
|
3e55492318 | ||
|
|
bb53b0167b | ||
|
|
ec07c402c9 | ||
|
|
cb50bccbeb | ||
|
|
bf7c7ebc82 | ||
|
|
1184c9f506 | ||
|
|
f781aaa6f5 | ||
|
|
e78b245eea | ||
|
|
bb1ec22197 | ||
|
|
1fff0f6a1c | ||
|
|
d28d34fad1 | ||
|
|
40ae83dcad | ||
|
|
0a18f43d95 | ||
|
|
c639bfb227 | ||
|
|
014866c330 | ||
|
|
dfe7b2e4a6 | ||
|
|
f371c05446 | ||
|
|
d31f6688f8 | ||
|
|
3e5b740ef0 | ||
|
|
f282265fbb |
@@ -35,7 +35,8 @@ const SCOPES = [
|
||||
"common",
|
||||
"global",
|
||||
"docs",
|
||||
"themebuilder"
|
||||
"themebuilder",
|
||||
"intl"
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,8 +1,6 @@
|
||||
name: Bug Report
|
||||
description: Are you facing a bug or a crash in Notesnook?
|
||||
labels: ["Type: Bug", "Status: Pending"]
|
||||
assignees:
|
||||
- thecodrr
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Notesnook Discord Community
|
||||
url: https://discord.gg/6mHHyncHJE
|
||||
url: https://go.notesnook.com/discord
|
||||
about: Reach out to us directly & discuss your issues, suggestions & other feedback!
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Feature request
|
||||
description: Suggest a feature you are missing in Notesnook
|
||||
labels: ["Type: Feature Request", "Status: Pending"]
|
||||
type: Feature
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
100
.github/workflows/android.e2e.yml
vendored
Normal file
100
.github/workflows/android.e2e.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
name: e2e-android
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
e2e-android:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
API_LEVEL: 34
|
||||
ARCH: x86_64
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Enable KVM
|
||||
run: |
|
||||
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
|
||||
sudo udevadm control --reload-rules
|
||||
sudo udevadm trigger --name-match=kvm
|
||||
|
||||
- name: Free Disk Space
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
tool-cache: false
|
||||
android: false
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install node modules
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=mobile
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v3
|
||||
with:
|
||||
gradle-version: wrapper
|
||||
cache-read-only: false
|
||||
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Install Detox CLI
|
||||
run: npm install detox-cli --global
|
||||
|
||||
- name: Detox build
|
||||
run: |
|
||||
yarn build:android
|
||||
ls apps/mobile/native/android/app/build/outputs/apk
|
||||
ls apps/mobile/native/android/app/build/outputs/apk/release
|
||||
|
||||
- name: Get device name
|
||||
id: device
|
||||
run: |
|
||||
AVD_NAME=$(node -p "require('./apps/mobile/native/.detoxrc.js').devices.emulator.device.avdName")
|
||||
echo "AVD_NAME=$AVD_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: AVD cache
|
||||
uses: actions/cache@v4
|
||||
id: avd-cache
|
||||
with:
|
||||
path: |
|
||||
~/.android/avd/*
|
||||
~/.android/adb*
|
||||
key: ${{ steps.device.outputs.AVD_NAME }}
|
||||
|
||||
- name: create AVD and generate snapshot for caching
|
||||
if: steps.avd-cache.outputs.cache-hit != 'true'
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ env.API_LEVEL }}
|
||||
arch: ${{ env.ARCH }}
|
||||
avd-name: ${{ steps.device.outputs.AVD_NAME }}
|
||||
force-avd-creation: false
|
||||
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
disable-animations: true
|
||||
script: echo "Generated AVD snapshot for caching."
|
||||
|
||||
- name: Detox test
|
||||
uses: reactivecircus/android-emulator-runner@v2
|
||||
with:
|
||||
api-level: ${{ env.API_LEVEL }}
|
||||
arch: ${{ env.ARCH }}
|
||||
avd-name: ${{ steps.device.outputs.AVD_NAME }}
|
||||
disable-animations: true
|
||||
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
|
||||
force-avd-creation: false
|
||||
script: yarn test:android --headless --record-logs failing --record-videos failing --take-screenshots failing
|
||||
|
||||
- name: Upload artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: detox-artifacts
|
||||
path: artifacts
|
||||
retention-days: 14
|
||||
2
.github/workflows/core.tests.yml
vendored
2
.github/workflows/core.tests.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
46
.github/workflows/desktop.publish.yml
vendored
46
.github/workflows/desktop.publish.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -84,6 +84,26 @@ jobs:
|
||||
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
|
||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
||||
|
||||
- name: Generate flatpak sources
|
||||
if: inputs.publish-github && inputs.build-linux
|
||||
run: |
|
||||
sudo apt-get install -y flatpak flatpak-builder pipx
|
||||
git clone https://github.com/flatpak/flatpak-builder-tools.git /tmp/flatpak-builder-tools
|
||||
cd /tmp/flatpak-builder-tools/node
|
||||
pipx install .
|
||||
cd -
|
||||
node scripts/generate-sources.mjs
|
||||
|
||||
- name: Upload flatpak sources
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: inputs.publish-github && inputs.build-linux
|
||||
with:
|
||||
draft: true
|
||||
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
|
||||
tag_name: v${{ steps.app_metadata.outputs.app_version }}
|
||||
files: ./generated-sources.json
|
||||
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
|
||||
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
needs: build
|
||||
@@ -92,7 +112,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -136,6 +156,12 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Install provisioning profile
|
||||
run: echo "${{ secrets.MAC_PROVISIONING_PROFILE }}" | base64 --decode > embedded.provisionprofile
|
||||
working-directory: ./apps/desktop
|
||||
@@ -188,7 +214,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -212,6 +238,12 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
@@ -247,7 +279,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET Core SDK
|
||||
uses: actions/setup-dotnet@v2
|
||||
@@ -272,6 +304,12 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
|
||||
|
||||
233
.github/workflows/desktop.tests.yml
vendored
Normal file
233
.github/workflows/desktop.tests.yml
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
name: Test @notesnook/desktop
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "app/desktop/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/desktop.tests.yml"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Generate desktop build
|
||||
run: npx nx build:desktop @notesnook/web
|
||||
|
||||
- name: Archive build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: apps/web/build/**/*
|
||||
|
||||
test-macos-x64:
|
||||
name: Test macOS x64
|
||||
needs: build
|
||||
runs-on: macos-13
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --mac --dir --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests x64
|
||||
run: |
|
||||
EXECUTABLE_PATH=output/mac/Notesnook.app/Contents/MacOS/Notesnook npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results-macos-x64
|
||||
path: apps/desktop/test-results
|
||||
retention-days: 5
|
||||
|
||||
test-macos:
|
||||
name: Test macOS
|
||||
needs: build
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --mac --dir --arm64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests arm64
|
||||
run: |
|
||||
EXECUTABLE_PATH=output/mac-arm64/Notesnook.app/Contents/MacOS/Notesnook npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results-macos
|
||||
path: apps/desktop/test-results
|
||||
retention-days: 5
|
||||
|
||||
test-linux:
|
||||
name: Test for Linux
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
yarn electron-builder --linux --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
EXECUTABLE_PATH=output/linux-unpacked/notesnook xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results-linux
|
||||
path: apps/desktop/test-results
|
||||
retention-days: 5
|
||||
|
||||
test-windows:
|
||||
name: Test for Windows
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: ./apps/web/build
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=desktop
|
||||
|
||||
- name: Install sqlite-better-trigram for all arch
|
||||
run: |
|
||||
npm i --cpu arm64 sqlite-better-trigram
|
||||
npm i --cpu x64 sqlite-better-trigram
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build Electron wrapper
|
||||
run: npx nx run release --project @notesnook/desktop
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
npx cross-env NOTESNOOK_STAGING=true yarn electron-builder --win --dir --arm64 --x64
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
npx cross-env EXECUTABLE_PATH=output/win-unpacked/Notesnook.exe npm run test
|
||||
working-directory: ./apps/desktop
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: test-results-win
|
||||
path: apps/desktop/test-results
|
||||
retention-days: 5
|
||||
2
.github/workflows/editor.tests.yml
vendored
2
.github/workflows/editor.tests.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
4
.github/workflows/help.publish.yml
vendored
4
.github/workflows/help.publish.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Publish Notesnook Help
|
||||
|
||||
on:
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
2
.github/workflows/ios.publish.yml
vendored
2
.github/workflows/ios.publish.yml
vendored
@@ -4,7 +4,7 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-13
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 60
|
||||
|
||||
steps:
|
||||
|
||||
12
.github/workflows/monograph.publish.yml
vendored
12
.github/workflows/monograph.publish.yml
vendored
@@ -32,6 +32,12 @@ jobs:
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=monograph
|
||||
|
||||
- name: Collect package metadata
|
||||
id: package_metadata
|
||||
working-directory: ./apps/monograph
|
||||
run: |
|
||||
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
|
||||
|
||||
- name: Generate build
|
||||
run: npx nx build @notesnook/monograph
|
||||
|
||||
@@ -55,8 +61,8 @@ jobs:
|
||||
run: docker pull streetwriters/monograph:latest
|
||||
|
||||
# Setup QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
# - name: Set up QEMU
|
||||
# uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
@@ -72,7 +78,7 @@ jobs:
|
||||
file: apps/monograph/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
|
||||
cache-from: streetwriters/monograph:latest
|
||||
|
||||
- name: Generate artifact attestation
|
||||
|
||||
2
.github/workflows/theme-builder.publish.yml
vendored
2
.github/workflows/theme-builder.publish.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
2
.github/workflows/vericrypt.publish.yml
vendored
2
.github/workflows/vericrypt.publish.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
44
.github/workflows/web.benchmarks.yml
vendored
Normal file
44
.github/workflows/web.benchmarks.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Benchmark @notesnook/web
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
# re-run workflow if workflow file changes
|
||||
- ".github/workflows/web.benchmarks.yml"
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
bench:
|
||||
name: Bench
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=web
|
||||
|
||||
- name: Setup environment
|
||||
run: |
|
||||
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build
|
||||
run: npm run build:web
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
working-directory: apps/web
|
||||
|
||||
- name: Run benchmarks
|
||||
id: benchmark
|
||||
run: node __bench__/app.bench.mjs
|
||||
working-directory: apps/web
|
||||
2
.github/workflows/web.publish.yml
vendored
2
.github/workflows/web.publish.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
4
.github/workflows/web.tests.yml
vendored
4
.github/workflows/web.tests.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download build
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
53
apps/desktop/__tests__/launch.test.mjs
Normal file
53
apps/desktop/__tests__/launch.test.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import { launchApp } from "./utils.mjs";
|
||||
import assert from "assert";
|
||||
import slugify from "slugify";
|
||||
import path from "path";
|
||||
import { mkdir } from "fs/promises";
|
||||
|
||||
test("make sure app loads", async (t) => {
|
||||
const { app, page } = await launchApp();
|
||||
try {
|
||||
await page.waitForSelector("#authForm");
|
||||
|
||||
assert.ok(
|
||||
await page.getByRole("button", { name: "Create account" }).isVisible()
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "Skip & go directly to the app" })
|
||||
.click();
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
} catch (e) {
|
||||
await mkdir("test-results", { recursive: true });
|
||||
await page.screenshot({
|
||||
path: path.join(
|
||||
"test-results",
|
||||
`${slugify(t.name)}-${process.platform}-${process.arch}-error.png`
|
||||
)
|
||||
});
|
||||
throw e;
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
38
apps/desktop/__tests__/utils.mjs
Normal file
38
apps/desktop/__tests__/utils.mjs
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { _electron as electron } from "playwright";
|
||||
|
||||
const executablePath = process.env.EXECUTABLE_PATH;
|
||||
|
||||
// process.platform === "linux"
|
||||
// ? "output/linux-unpacked/Notesnook"
|
||||
// : process.platform === "darwin"
|
||||
// ? "output/mac/Notesnook.app/Contents/MacOS/Notesnook"
|
||||
// : "output/win-unpacked/Notesnook.exe";
|
||||
|
||||
export async function launchApp() {
|
||||
const app = await electron.launch({
|
||||
executablePath
|
||||
});
|
||||
|
||||
const page = await app.firstWindow();
|
||||
|
||||
return { app, page };
|
||||
}
|
||||
3841
apps/desktop/package-lock.json
generated
3841
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,21 +2,38 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.18",
|
||||
"version": "3.0.21",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"main": "./dist/cjs/index.js",
|
||||
"module": "./dist/esm/index.js",
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"exports": {
|
||||
".": {
|
||||
"require": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/cjs/index.js"
|
||||
},
|
||||
"import": {
|
||||
"types": "./dist/types/index.d.ts",
|
||||
"default": "./dist/esm/index.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"homepage": "https://notesnook.com/",
|
||||
"repository": "https://github.com/streetwriters/notesnook",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@lingui/core": "4.11.4",
|
||||
"@notesnook/intl": "file:../../packages/intl",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"better-sqlite3-multiple-ciphers": "11.2.1",
|
||||
"better-sqlite3-multiple-ciphers": "11.5.0",
|
||||
"electron-trpc": "0.6.1",
|
||||
"electron-updater": "^6.3.4",
|
||||
"icojs": "^0.19.4",
|
||||
"sqlite-better-trigram": "^0.0.2",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "^3.23.8"
|
||||
@@ -26,14 +43,17 @@
|
||||
"@types/node": "22.7.0",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.1",
|
||||
"electron": "^30.5.1",
|
||||
"electron-builder": "^25.0.5",
|
||||
"electron": "^31.7.4",
|
||||
"electron-builder": "^25.1.8",
|
||||
"esbuild": "^0.24.0",
|
||||
"node-abi": "^3.68.0",
|
||||
"node-gyp-build": "^4.8.2",
|
||||
"playwright": "^1.48.2",
|
||||
"prebuildify": "^6.0.1",
|
||||
"slugify": "^1.6.6",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^6.19.8"
|
||||
"undici": "^6.19.8",
|
||||
"vitest": "^2.1.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
@@ -42,10 +62,11 @@
|
||||
"start": "node scripts/dev.mjs",
|
||||
"staging": "node scripts/build.mjs --run",
|
||||
"release": "node scripts/build.mjs",
|
||||
"build": "tsc",
|
||||
"build": "node ../../scripts/build.mjs",
|
||||
"bundle": "esbuild electron=./src/main.ts ./src/preload.ts --external:electron --external:fsevents --external:better-sqlite3-multiple-ciphers --external:sodium-native --minify --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=false --define:RELEASE=true",
|
||||
"bundle:mas": "esbuild electron=./src/main.ts ./src/preload.ts --minify --external:electron --external:fsevents --bundle --outdir=./build --platform=node --tsconfig=tsconfig.json --define:MAC_APP_STORE=true --define:RELEASE=true",
|
||||
"postinstall": "patch-package"
|
||||
"postinstall": "patch-package",
|
||||
"test": "node --test --test-timeout 30000 --test-force-exit"
|
||||
},
|
||||
"author": {
|
||||
"name": "Streetwriters (Private) Limited",
|
||||
@@ -74,7 +95,20 @@
|
||||
"node_modules/file-uri-to-path",
|
||||
"node_modules/bindings",
|
||||
"node_modules/node-gyp-build",
|
||||
"node_modules/sqlite-better-trigram",
|
||||
"node_modules/sodium-native/prebuilds/${platform}-${arch}",
|
||||
{
|
||||
"from": "node_modules/sqlite-better-trigram-linux-${arch}",
|
||||
"to": "node_modules/sqlite-better-trigram-linux-${arch}"
|
||||
},
|
||||
{
|
||||
"from": "node_modules/sqlite-better-trigram-darwin-${arch}",
|
||||
"to": "node_modules/sqlite-better-trigram-darwin-${arch}"
|
||||
},
|
||||
{
|
||||
"from": "node_modules/sqlite-better-trigram-windows-${arch}",
|
||||
"to": "node_modules/sqlite-better-trigram-windows-${arch}"
|
||||
},
|
||||
"node_modules/sodium-native/index.js",
|
||||
"node_modules/sodium-native/package.json"
|
||||
],
|
||||
@@ -147,10 +181,12 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"signingHashAlgorithms": [
|
||||
"sha256"
|
||||
],
|
||||
"sign": "./scripts/sign.js",
|
||||
"signtoolOptions": {
|
||||
"signingHashAlgorithms": [
|
||||
"sha256"
|
||||
],
|
||||
"sign": "./scripts/sign.js"
|
||||
},
|
||||
"icon": "assets/icons/app.ico"
|
||||
},
|
||||
"portable": {
|
||||
|
||||
@@ -63,7 +63,7 @@ if (args.variant === "mas") {
|
||||
await exec(`yarn run bundle`);
|
||||
}
|
||||
|
||||
await exec(`yarn tsc`);
|
||||
await exec(`yarn run build`);
|
||||
|
||||
if (args.run) {
|
||||
await exec(`yarn electron-builder --dir --x64`);
|
||||
@@ -94,7 +94,7 @@ async function patchBetterSQLite3() {
|
||||
);
|
||||
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
|
||||
|
||||
json.version = "11.2.2";
|
||||
json.version = "11.5.1";
|
||||
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
|
||||
json.repository.url =
|
||||
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
|
||||
|
||||
@@ -36,6 +36,8 @@ const ENV = {
|
||||
FORCE_COLOR: "false",
|
||||
COLOR: "0"
|
||||
};
|
||||
process.chdir(path.join(__dirname, ".."));
|
||||
|
||||
await onChange(true);
|
||||
|
||||
console.log("Watching...");
|
||||
@@ -53,11 +55,13 @@ async function onChange(first) {
|
||||
if (first) {
|
||||
await fs.rm("./build/", { force: true, recursive: true });
|
||||
|
||||
await exec("npm rebuild electron --verbose --foreground-scripts");
|
||||
|
||||
await exec("yarn electron-builder install-app-deps");
|
||||
}
|
||||
|
||||
await exec(`yarn run bundle`);
|
||||
execAsync(`yarn`, [`tsc`]);
|
||||
await exec(`yarn run build`);
|
||||
|
||||
if (await isBundleSame()) {
|
||||
console.log("Bundle is same. Doing nothing.");
|
||||
|
||||
@@ -46,6 +46,8 @@ export class SQLite {
|
||||
this.sqlite = require("better-sqlite3-multiple-ciphers")(
|
||||
filePath
|
||||
).unsafeMode(true);
|
||||
const betterTrigram = require("sqlite-better-trigram");
|
||||
betterTrigram.load(this.sqlite);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./constants";
|
||||
export { PATHS } from "./constants";
|
||||
export type { AppRouter } from "./api";
|
||||
export { type UpdateInfo } from "builder-util-runtime";
|
||||
export { type DesktopIntegration } from "./utils/config";
|
||||
|
||||
@@ -36,6 +36,20 @@ import { bringToFront } from "./utils/bring-to-front";
|
||||
import { bridge } from "./api/bridge";
|
||||
import { setupDesktopIntegration } from "./utils/desktop-integration";
|
||||
import { disableCustomDns, enableCustomDns } from "./utils/custom-dns";
|
||||
import { Messages, setI18nGlobal } from "@notesnook/intl";
|
||||
import { i18n } from "@lingui/core";
|
||||
|
||||
const locale =
|
||||
process.env.NODE_ENV === "development"
|
||||
? import("@notesnook/intl/locales/$pseudo-LOCALE.json")
|
||||
: import("@notesnook/intl/locales/$en.json");
|
||||
locale.then(({ default: locale }) => {
|
||||
i18n.load({
|
||||
en: locale.messages as unknown as Messages
|
||||
});
|
||||
i18n.activate("en");
|
||||
});
|
||||
setI18nGlobal(i18n);
|
||||
|
||||
// only run a single instance
|
||||
if (!MAC_APP_STORE && !app.requestSingleInstanceLock()) {
|
||||
@@ -87,7 +101,7 @@ async function createWindow() {
|
||||
...(config.desktopSettings.nativeTitlebar
|
||||
? {}
|
||||
: {
|
||||
titleBarStyle: "hidden",
|
||||
titleBarStyle: process.platform === "win32" || process.platform === "darwin" ? "hidden" : "default",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
|
||||
@@ -19,22 +19,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
/* eslint-disable no-var */
|
||||
|
||||
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
|
||||
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
|
||||
// import type { NNCrypto } from "@notesnook/crypto";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { platform } from "os";
|
||||
|
||||
declare global {
|
||||
var os: () => "mas" | ReturnType<typeof platform>;
|
||||
var electronTRPC: RendererGlobalElectronTRPC;
|
||||
var electronTRPC: any;
|
||||
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
const electronTRPC: RendererGlobalElectronTRPC = {
|
||||
sendMessage: (operation) =>
|
||||
const electronTRPC = {
|
||||
sendMessage: (operation: any) =>
|
||||
ipcRenderer.send(ELECTRON_TRPC_CHANNEL, operation),
|
||||
onMessage: (callback) =>
|
||||
onMessage: (callback: any) =>
|
||||
ipcRenderer.on(ELECTRON_TRPC_CHANNEL, (_event, args) => callback(args))
|
||||
};
|
||||
globalThis.electronTRPC = electronTRPC;
|
||||
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { Menu, MenuItem, clipboard, shell } from "electron";
|
||||
|
||||
function setupMenu() {
|
||||
@@ -40,7 +41,7 @@ function setupMenu() {
|
||||
if (params.misspelledWord) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Add to dictionary",
|
||||
label: strings.addToDictionary(),
|
||||
click: () =>
|
||||
globalThis.window?.webContents.session.addWordToSpellCheckerDictionary(
|
||||
params.misspelledWord
|
||||
@@ -59,7 +60,7 @@ function setupMenu() {
|
||||
if (params.linkURL.length) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Open in browser",
|
||||
label: strings.openInBrowser(),
|
||||
click: () => shell.openExternal(params.linkURL)
|
||||
})
|
||||
);
|
||||
@@ -68,7 +69,7 @@ function setupMenu() {
|
||||
if (params.isEditable) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Undo",
|
||||
label: strings.undo(),
|
||||
role: "undo",
|
||||
enabled: params.isEditable,
|
||||
accelerator: "CommandOrControl+Z"
|
||||
@@ -77,7 +78,7 @@ function setupMenu() {
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Redo",
|
||||
label: strings.redo(),
|
||||
role: "redo",
|
||||
enabled: params.isEditable,
|
||||
accelerator: "CommandOrControl+Y"
|
||||
@@ -94,7 +95,7 @@ function setupMenu() {
|
||||
if (params.isEditable)
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Cut",
|
||||
label: strings.cut(),
|
||||
role: "cut",
|
||||
enabled: params.selectionText.length > 0,
|
||||
accelerator: "CommandOrControl+X"
|
||||
@@ -104,7 +105,7 @@ function setupMenu() {
|
||||
if (params.linkURL?.length) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Copy link",
|
||||
label: strings.copyLink(),
|
||||
click() {
|
||||
clipboard.writeText(params.linkURL);
|
||||
}
|
||||
@@ -113,7 +114,7 @@ function setupMenu() {
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Copy link text",
|
||||
label: strings.copyLinkText(),
|
||||
click() {
|
||||
clipboard.writeText(params.linkText);
|
||||
}
|
||||
@@ -124,7 +125,7 @@ function setupMenu() {
|
||||
if (params.selectionText.length) {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Copy",
|
||||
label: strings.copy(),
|
||||
role: "copy",
|
||||
accelerator: "CommandOrControl+C"
|
||||
})
|
||||
@@ -135,7 +136,7 @@ function setupMenu() {
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
id: "copy-image",
|
||||
label: "Copy Image",
|
||||
label: strings.copyImage(),
|
||||
click() {
|
||||
globalThis.window?.webContents.copyImageAt(params.x, params.y);
|
||||
}
|
||||
@@ -145,7 +146,7 @@ function setupMenu() {
|
||||
if (params.isEditable)
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: "Paste",
|
||||
label: strings.paste(),
|
||||
role: "pasteAndMatchStyle",
|
||||
enabled: clipboard.readText("clipboard").length > 0,
|
||||
accelerator: "CommandOrControl+V"
|
||||
|
||||
@@ -57,7 +57,8 @@ function getSystemTheme() {
|
||||
nativeTheme.themeSource = oldThemeSource;
|
||||
|
||||
setTimeout(
|
||||
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
|
||||
() =>
|
||||
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
|
||||
1000
|
||||
);
|
||||
return currentTheme;
|
||||
@@ -72,7 +73,8 @@ function changeTheme(theme: Theme) {
|
||||
nativeTheme.themeSource = theme;
|
||||
|
||||
setTimeout(
|
||||
() => listeners.forEach((a) => nativeTheme.addListener("updated", a)),
|
||||
() =>
|
||||
listeners.forEach((a) => nativeTheme.addListener("updated", () => a())),
|
||||
1000
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"lib": ["ESNext"]
|
||||
"lib": ["ESNext"],
|
||||
"moduleResolution": "Bundler"
|
||||
},
|
||||
"include": ["src", "global.d.ts"]
|
||||
}
|
||||
|
||||
1
apps/mobile/.gitignore
vendored
1
apps/mobile/.gitignore
vendored
@@ -39,6 +39,7 @@ DerivedData
|
||||
*.xcuserstate
|
||||
*.hprof
|
||||
**/.xcode.env.local
|
||||
native/cache
|
||||
# Android/IntelliJ
|
||||
#
|
||||
rn-build-deps/
|
||||
|
||||
@@ -16,6 +16,8 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { i18n } from "@lingui/core";
|
||||
import { I18nProvider } from "@lingui/react";
|
||||
import {
|
||||
THEME_COMPATIBILITY_VERSION,
|
||||
useThemeEngineStore
|
||||
@@ -63,7 +65,9 @@ const App = () => {
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%"
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -138,7 +142,11 @@ export const withTheme = (Element: () => JSX.Element) => {
|
||||
currTheme = nextTheme;
|
||||
}
|
||||
|
||||
return <Element />;
|
||||
return (
|
||||
<I18nProvider i18n={i18n}>
|
||||
<Element />
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -33,11 +33,11 @@ import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export async function setupDatabase(password) {
|
||||
const key = await getDatabaseKey(password);
|
||||
if (!key)
|
||||
throw new Error("Database setup failed, could not get database key");
|
||||
if (!key) throw new Error(strings.databaseSetupFailed());
|
||||
|
||||
console.log("Opening database with key:", !!key);
|
||||
|
||||
@@ -54,10 +54,10 @@ export async function setupDatabase(password) {
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
fs: filesystem,
|
||||
compressor: {
|
||||
compressor: () => ({
|
||||
compress: Gzip.deflate,
|
||||
decompress: Gzip.inflate
|
||||
},
|
||||
}),
|
||||
batchSize: 100,
|
||||
sqliteOptions: {
|
||||
dialect: (name) => ({
|
||||
|
||||
@@ -24,6 +24,7 @@ import type {
|
||||
} from "@streetwriters/kysely";
|
||||
import { CompiledQuery } from "@streetwriters/kysely";
|
||||
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type Config = { dbName: string; async: boolean; location: string };
|
||||
|
||||
@@ -97,7 +98,7 @@ class RNSqliteConnection implements DatabaseConnection {
|
||||
constructor(private readonly db: QuickSQLiteConnection) {}
|
||||
|
||||
streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
||||
throw new Error("wasqlite driver doesn't support streaming");
|
||||
throw new Error(strings.streamingNotSupported());
|
||||
}
|
||||
|
||||
async executeQuery<R>(
|
||||
|
||||
@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { getFileNameWithExtension } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
@@ -89,7 +90,7 @@ export async function downloadAttachments(attachments) {
|
||||
current: 0,
|
||||
total: 0,
|
||||
success: false,
|
||||
message: "Download cancelled",
|
||||
message: strings.network.dowloadCancelled(),
|
||||
canceled: true
|
||||
});
|
||||
return true;
|
||||
@@ -116,7 +117,7 @@ export async function downloadAttachments(attachments) {
|
||||
|
||||
if (isCancelled()) return;
|
||||
|
||||
if (!uri) throw new Error("Failed to download file");
|
||||
if (!uri) throw new Error(strings.failedToDownloadFile());
|
||||
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||
const filePath = `${zipSourceFolder}/${attachment.filename}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||
@@ -139,7 +140,7 @@ export async function downloadAttachments(attachments) {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
current: 0,
|
||||
total: 1,
|
||||
message: "Saving zip file... Please wait",
|
||||
message: strings.savingZipFile(),
|
||||
groupId
|
||||
});
|
||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||
@@ -149,9 +150,7 @@ export async function downloadAttachments(attachments) {
|
||||
groupId,
|
||||
current: progress,
|
||||
total: 1,
|
||||
message: `Saving zip file (${(progress * 100).toFixed(
|
||||
1
|
||||
)}%)... Please wait`
|
||||
message: strings.savingZipFile((progress * 100).toFixed(1))
|
||||
});
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
@@ -245,7 +244,7 @@ export default async function downloadAttachment(
|
||||
);
|
||||
}
|
||||
|
||||
let filename = getFileNameWithExtension(
|
||||
let filename = await getFileNameWithExtension(
|
||||
attachment.filename,
|
||||
attachment.mimeType
|
||||
);
|
||||
@@ -273,8 +272,8 @@ export default async function downloadAttachment(
|
||||
|
||||
if (!options.silent) {
|
||||
ToastManager.show({
|
||||
heading: "Download successful",
|
||||
message: filename + " downloaded",
|
||||
heading: strings.network.downloadSuccess(),
|
||||
message: strings.network.fileDownloaded(filename),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
@@ -284,12 +283,8 @@ export default async function downloadAttachment(
|
||||
}
|
||||
if (!options.silent) {
|
||||
presentSheet({
|
||||
title: "File downloaded",
|
||||
paragraph: `${filename} saved to ${
|
||||
Platform.OS === "android"
|
||||
? "selected path"
|
||||
: "File Manager/Notesnook/downloads"
|
||||
}`,
|
||||
title: strings.network.fileDownloaded(),
|
||||
paragraph: strings.fileSaved(filename, Platform.OS),
|
||||
icon: "download",
|
||||
context: global ? null : attachment.hash,
|
||||
component: <ShareComponent uri={fileUri} name={filename} padding={12} />
|
||||
|
||||
@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
@@ -48,12 +49,12 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
const size = await getUploadedFileSize(filename);
|
||||
|
||||
if (size === -1) {
|
||||
const error = `Uploaded file verification failed. (File hash: ${filename})`;
|
||||
const error = `${strings.fileVerificationFailed()} (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (size === 0) {
|
||||
const error = `File length is 0. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
const error = `${strings.fileLengthError()} (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
@@ -62,7 +63,10 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
|
||||
if (attachment && attachment.size !== decryptedLength) {
|
||||
const error = `File length mismatch. Expected ${attachment.size} but got ${decryptedLength} bytes. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
const error = `${strings.fileLengthMismatch(
|
||||
attachment.size,
|
||||
decryptedLength
|
||||
)} (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
@@ -76,7 +80,9 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, ${res.status}, ${res.statusText}, reason: Unable to resolve download url`
|
||||
);
|
||||
throw new Error(`${res.status}: Unable to resolve download url`);
|
||||
throw new Error(
|
||||
`${res.status}: ${strings.failedToResolvedDownloadUrl()}`
|
||||
);
|
||||
}
|
||||
|
||||
const downloadUrl = await res.text();
|
||||
@@ -85,7 +91,7 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: Unable to resolve download url`
|
||||
);
|
||||
throw new Error("Unable to resolve download url");
|
||||
throw new Error(strings.failedToResolvedDownloadUrl());
|
||||
}
|
||||
|
||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
||||
@@ -138,18 +144,15 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
|
||||
ToastManager.show({
|
||||
heading: "Error downloading file",
|
||||
const toast = {
|
||||
heading: strings.downloadError(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
ToastManager.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
ToastManager.show(toast);
|
||||
toast.context = "local";
|
||||
ToastManager.show(toast);
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
@@ -126,9 +126,13 @@ export async function clearFileStorage() {
|
||||
}
|
||||
|
||||
export async function createCacheDir() {
|
||||
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
|
||||
await RNFetchBlob.fs.mkdir(cacheDir);
|
||||
DatabaseLogger.log("Cache directory created");
|
||||
try {
|
||||
if (!(await RNFetchBlob.fs.exists(cacheDir))) {
|
||||
await RNFetchBlob.fs.mkdir(cacheDir);
|
||||
DatabaseLogger.log("Cache directory created");
|
||||
}
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,22 +235,30 @@ export async function exists(filename) {
|
||||
}
|
||||
|
||||
export async function bulkExists(files) {
|
||||
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
|
||||
try {
|
||||
await createCacheDir();
|
||||
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
|
||||
missingFiles = missingFiles.filter((file) => !appGroupFiles.includes(file));
|
||||
if (Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
|
||||
missingFiles = missingFiles.filter(
|
||||
(file) => !appGroupFiles.includes(file)
|
||||
);
|
||||
}
|
||||
return missingFiles;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return [];
|
||||
}
|
||||
|
||||
return missingFiles;
|
||||
}
|
||||
|
||||
export async function getCacheSize() {
|
||||
await createCacheDir();
|
||||
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
|
||||
let total = 0;
|
||||
console.log("Total files", stat.length);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir } from "./io";
|
||||
import { cacheDir, getUploadedFileSize } from "./utils";
|
||||
import { cacheDir, checkUpload, getUploadedFileSize } from "./utils";
|
||||
|
||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) return false;
|
||||
@@ -33,24 +33,42 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
try {
|
||||
const uploadedFileSize = await getUploadedFileSize(filename);
|
||||
|
||||
if (uploadedFileSize === -1) {
|
||||
const error = `Uploaded file verification failed. (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
let filePath = `${cacheDir}/${filename}`;
|
||||
let exists = await RNFetchBlob.fs.exists(filePath);
|
||||
// Check for file in appGroupPath if it doesn't exist in cacheDir
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
filePath = appGroupPath;
|
||||
exists = await RNFetchBlob.fs.exists(filePath);
|
||||
}
|
||||
|
||||
if (uploadedFileSize !== 0) {
|
||||
if (!exists) {
|
||||
throw new Error(
|
||||
`Trying to upload file at path ${filePath} that doest not exist.`
|
||||
);
|
||||
}
|
||||
|
||||
const fileSize = (await RNFetchBlob.fs.stat(filePath)).size;
|
||||
|
||||
let remoteFileSize = await getUploadedFileSize(filename);
|
||||
if (remoteFileSize === -1) return false;
|
||||
if (remoteFileSize > 0 && remoteFileSize === fileSize) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
let res = await fetch(url, {
|
||||
let uploadUrlResponse = await fetch(url, {
|
||||
method: "PUT",
|
||||
headers
|
||||
});
|
||||
|
||||
const uploadUrl = res.ok ? await res.text() : await res.json();
|
||||
const uploadUrl = uploadUrlResponse.ok
|
||||
? await uploadUrlResponse.text()
|
||||
: await uploadUrlResponse.json();
|
||||
|
||||
if (typeof uploadUrl !== "string") {
|
||||
throw new Error(
|
||||
@@ -58,28 +76,9 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
);
|
||||
}
|
||||
|
||||
let uploadFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${filename}`;
|
||||
let exists = await RNFetchBlob.fs.exists(uploadFilePath);
|
||||
if (!exists && Platform.OS === "ios") {
|
||||
uploadFilePath = appGroupPath;
|
||||
exists = await RNFetchBlob.fs.exists(uploadFilePath);
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
throw new Error(
|
||||
`Trying to upload file at path ${uploadFilePath} that doest not exist.`
|
||||
);
|
||||
}
|
||||
|
||||
DatabaseLogger.info(`Starting upload: ${filename}`);
|
||||
|
||||
let request = RNFetchBlob.config({
|
||||
let uploadRequest = RNFetchBlob.config({
|
||||
IOSBackgroundTask: !globalThis["IS_SHARE_EXTENSION"]
|
||||
})
|
||||
.fetch(
|
||||
@@ -88,7 +87,7 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
{
|
||||
"content-type": ""
|
||||
},
|
||||
RNFetchBlob.wrap(uploadFilePath)
|
||||
RNFetchBlob.wrap(filePath)
|
||||
)
|
||||
.uploadProgress((sent, total) => {
|
||||
useAttachmentStore
|
||||
@@ -101,30 +100,27 @@ export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
uploadRequest.cancel();
|
||||
};
|
||||
let response = await request;
|
||||
|
||||
let status = response.info().status;
|
||||
let text = await response.text();
|
||||
let result = status >= 200 && status < 300 && text.length === 0;
|
||||
let uploadResponse = await uploadRequest;
|
||||
let status = uploadResponse.info().status;
|
||||
let uploaded = status >= 200 && status < 300;
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
if (result) {
|
||||
DatabaseLogger.info(
|
||||
`File upload status: ${filename}, ${status}, ${text}`
|
||||
);
|
||||
let attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return result;
|
||||
} else {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
|
||||
|
||||
if (!uploaded) {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(filePath);
|
||||
throw new Error(
|
||||
`${status}, ${text}, name: ${fileInfo.filename}, length: ${
|
||||
`${status}, name: ${fileInfo.filename}, length: ${
|
||||
fileInfo.size
|
||||
}, info: ${JSON.stringify(response.info())}`
|
||||
}, info: ${JSON.stringify(uploadResponse.info())}`
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
await checkUpload(filename, requestOptions.chunkSize, attachment.size);
|
||||
DatabaseLogger.info(`File upload status: ${filename}, ${status}`);
|
||||
return uploaded;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
ToastManager.error(e, "File upload failed");
|
||||
|
||||
@@ -123,3 +123,18 @@ export async function getUploadedFileSize(hash) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkUpload(filename, chunkSize, expectedSize) {
|
||||
const size = await getUploadedFileSize(filename);
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
const error =
|
||||
size === 0
|
||||
? `File size is 0.`
|
||||
: size === -1
|
||||
? `File verification check failed.`
|
||||
: expectedSize !== decryptedLength
|
||||
? `File size mismatch. Expected ${size} bytes but got ${decryptedLength} bytes.`
|
||||
: undefined;
|
||||
if (error) throw new Error(error);
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const getUser = () => {
|
||||
const user = MMKV.getString("user");
|
||||
@@ -157,7 +158,7 @@ const AppLockedOverlay = () => {
|
||||
password.current = undefined;
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: `Invalid ${keyboardType === "numeric" ? "pin" : "password"}`,
|
||||
heading: strings.invalid(keyboardType),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -250,7 +251,7 @@ const AppLockedOverlay = () => {
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Unlock your notes
|
||||
{strings.unlockNotes()}
|
||||
</Heading>
|
||||
|
||||
<Paragraph
|
||||
@@ -260,7 +261,7 @@ const AppLockedOverlay = () => {
|
||||
maxWidth: "90%"
|
||||
}}
|
||||
>
|
||||
{"Please verify it's you"}
|
||||
{strings.verifyItsYou()}
|
||||
</Paragraph>
|
||||
<Seperator />
|
||||
<View
|
||||
@@ -278,13 +279,13 @@ const AppLockedOverlay = () => {
|
||||
keyboardType={
|
||||
appLockHasPasswordSecurity ? keyboardType : "default"
|
||||
}
|
||||
placeholder={`Enter ${
|
||||
placeholder={
|
||||
appLockHasPasswordSecurity
|
||||
? `app lock ${
|
||||
keyboardType === "numeric" ? "pin" : "password"
|
||||
}`
|
||||
: "account password"
|
||||
}`}
|
||||
? keyboardType === "numeric"
|
||||
? strings.enterApplockPassword()
|
||||
: strings.enterApplockPin()
|
||||
: strings.enterAccountPassword()
|
||||
}
|
||||
onChangeText={(v) => (password.current = v)}
|
||||
onSubmit={() => {
|
||||
onSubmit();
|
||||
@@ -301,7 +302,7 @@ const AppLockedOverlay = () => {
|
||||
{user || appLockHasPasswordSecurity ? (
|
||||
<>
|
||||
<Button
|
||||
title="Continue"
|
||||
title={strings.continue()}
|
||||
type="accent"
|
||||
onPress={onSubmit}
|
||||
width={250}
|
||||
@@ -317,7 +318,7 @@ const AppLockedOverlay = () => {
|
||||
|
||||
{biometricsAuthEnabled ? (
|
||||
<Button
|
||||
title="Unlock with Biometrics"
|
||||
title={strings.unlockWithBiometrics()}
|
||||
width={250}
|
||||
onPress={onUnlockAppRequested}
|
||||
icon={"fingerprint"}
|
||||
|
||||
@@ -57,6 +57,7 @@ import { Notice } from "../ui/notice";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
@@ -81,7 +82,7 @@ const Actions = ({
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: "Download",
|
||||
name: strings.network.download(),
|
||||
onPress: async () => {
|
||||
if (currentProgress) {
|
||||
await db.fs().cancel(attachment.hash);
|
||||
@@ -93,11 +94,11 @@ const Actions = ({
|
||||
icon: "download"
|
||||
},
|
||||
{
|
||||
name: "Reupload",
|
||||
name: strings.network.reupload(),
|
||||
onPress: async () => {
|
||||
if (!PremiumService.get()) {
|
||||
ToastManager.show({
|
||||
heading: "Upgrade to pro",
|
||||
heading: strings.upgradeToPro(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -113,10 +114,10 @@ const Actions = ({
|
||||
icon: "upload"
|
||||
},
|
||||
{
|
||||
name: "Run file check",
|
||||
name: strings.fileCheck(),
|
||||
onPress: async () => {
|
||||
setLoading({
|
||||
name: "Run file check"
|
||||
name: strings.fileCheck()
|
||||
});
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
@@ -125,7 +126,7 @@ const Actions = ({
|
||||
db.attachments.markAsFailed(attachment.id, result.failed);
|
||||
setFailed(result.failed);
|
||||
ToastManager.show({
|
||||
heading: "File check failed with error: " + result.failed,
|
||||
heading: strings.fileCheckFailed(result.failed),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -134,7 +135,7 @@ const Actions = ({
|
||||
db.attachments.markAsFailed(attachment.id);
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
ToastManager.show({
|
||||
heading: "File check passed",
|
||||
heading: strings.fileCheckPassed(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
@@ -148,12 +149,11 @@ const Actions = ({
|
||||
icon: "file-check"
|
||||
},
|
||||
{
|
||||
name: "Rename",
|
||||
onPress: async () => {
|
||||
name: strings.rename(),
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
input: true,
|
||||
title: "Rename file",
|
||||
paragraph: "Enter a new name for the file",
|
||||
title: strings.renameFile(),
|
||||
defaultValue: attachment.filename,
|
||||
positivePress: async (value) => {
|
||||
if (value && value.length > 0) {
|
||||
@@ -166,13 +166,13 @@ const Actions = ({
|
||||
eSendEvent(eDBItemUpdate, attachment.id);
|
||||
}
|
||||
},
|
||||
positiveText: "Rename"
|
||||
positiveText: strings.rename()
|
||||
});
|
||||
},
|
||||
icon: "form-textbox"
|
||||
},
|
||||
{
|
||||
name: "Delete",
|
||||
name: strings.delete(),
|
||||
onPress: async () => {
|
||||
const relations = await db.relations.to(attachment, "note").get();
|
||||
await db.attachments.remove(attachment.hash, false);
|
||||
@@ -256,8 +256,7 @@ const Actions = ({
|
||||
|
||||
{notes.length ? (
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{notes.length} note
|
||||
{notes.length > 1 ? "s" : ""}
|
||||
{strings.notes(notes.length)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<Paragraph
|
||||
@@ -265,7 +264,7 @@ const Actions = ({
|
||||
Clipboard.setString(attachment.hash);
|
||||
ToastManager.show({
|
||||
type: "success",
|
||||
heading: "Attachment hash copied",
|
||||
heading: strings.hashCopied(),
|
||||
context: "local"
|
||||
});
|
||||
}}
|
||||
@@ -295,7 +294,7 @@ const Actions = ({
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
List of notes:
|
||||
{strings.listOf()} {strings.dataTypesPlural.note()}:
|
||||
</Heading>
|
||||
|
||||
{notes.map((item) => (
|
||||
@@ -354,7 +353,7 @@ const Actions = ({
|
||||
{failed ? (
|
||||
<Notice
|
||||
type="alert"
|
||||
text={`File check failed: ${failed} Try reuploading the file to fix the issue.`}
|
||||
text={strings.fileCheckFailed(failed)}
|
||||
size="small"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { IconButton } from "../ui/icon-button";
|
||||
import { ProgressCircleComponent } from "../ui/svg/lazy";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import Actions from "./actions";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
function getFileExtension(filename: string) {
|
||||
const ext = /^.+\.([^.]+)$/.exec(filename);
|
||||
@@ -136,7 +137,7 @@ export const AttachmentItem = ({
|
||||
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}>
|
||||
File size: {formatBytes(attachment.size)}
|
||||
{strings.fileSize()}: {formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
SortOptions,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
@@ -94,31 +95,31 @@ const useRechecker = create<RecheckerState>((set) => ({
|
||||
|
||||
const attachmentTypes = [
|
||||
{
|
||||
title: "All",
|
||||
title: strings.mediaTypes.all(),
|
||||
filterBy: "all"
|
||||
},
|
||||
{
|
||||
title: "Images",
|
||||
title: strings.mediaTypes.image(),
|
||||
filterBy: "images"
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
filterBy: "documents"
|
||||
},
|
||||
{
|
||||
title: "Video",
|
||||
filterBy: "video"
|
||||
},
|
||||
{
|
||||
title: "Audio",
|
||||
title: strings.mediaTypes.audio(),
|
||||
filterBy: "audio"
|
||||
},
|
||||
{
|
||||
title: "Orphaned",
|
||||
title: strings.mediaTypes.video(),
|
||||
filterBy: "video"
|
||||
},
|
||||
{
|
||||
title: strings.mediaTypes.document(),
|
||||
filterBy: "documents"
|
||||
},
|
||||
{
|
||||
title: strings.mediaTypes.orphaned(),
|
||||
filterBy: "orphaned"
|
||||
},
|
||||
{
|
||||
title: "Errors",
|
||||
title: strings.mediaTypes.errors(),
|
||||
filterBy: "errors"
|
||||
}
|
||||
];
|
||||
@@ -234,7 +235,7 @@ export const AttachmentDialog = ({
|
||||
for (let i = 0; i < filteredAttachments.placeholders.length; i++) {
|
||||
if (!getState().isWorking) {
|
||||
ToastManager.show({
|
||||
message: "Attachment recheck cancelled",
|
||||
message: strings.attachmentRecheckCancelled(),
|
||||
type: "info",
|
||||
context: isSheet ? "local" : "global"
|
||||
});
|
||||
@@ -321,7 +322,7 @@ export const AttachmentDialog = ({
|
||||
) : null}
|
||||
{!isSheet ? (
|
||||
<Header
|
||||
title="Manage attachments"
|
||||
title={strings.manageAttachments()}
|
||||
renderedInRoute="SettingsGroup"
|
||||
canGoBack
|
||||
headerRightButtons={[
|
||||
@@ -329,23 +330,23 @@ export const AttachmentDialog = ({
|
||||
onPress() {
|
||||
onCheck();
|
||||
},
|
||||
title: "Recheck all"
|
||||
title: strings.recheckAll()
|
||||
},
|
||||
{
|
||||
onPress() {
|
||||
if (!attachments) return;
|
||||
presentDialog({
|
||||
title: `Download ${attachments.placeholders.length} attachments`,
|
||||
paragraph:
|
||||
"Are you sure you want to download all attachments?",
|
||||
positiveText: "Download",
|
||||
title: strings.doActions.download.attachment(
|
||||
attachments.placeholders.length
|
||||
),
|
||||
positiveText: strings.network.download(),
|
||||
positivePress: async () => {
|
||||
downloadAttachments(await attachments.ids());
|
||||
},
|
||||
negativeText: "Cancel"
|
||||
negativeText: strings.cancel()
|
||||
});
|
||||
},
|
||||
title: "Download all"
|
||||
title: strings.downloadAllAttachments()
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@@ -358,7 +359,7 @@ export const AttachmentDialog = ({
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Heading>Attachments</Heading>
|
||||
<Heading>{strings.dataTypesPluralCamelCase.attachment()}</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
@@ -387,15 +388,14 @@ export const AttachmentDialog = ({
|
||||
onPress={() => {
|
||||
if (!attachments) return;
|
||||
presentDialog({
|
||||
title: `Download ${attachments.placeholders.length} attachments`,
|
||||
paragraph:
|
||||
"Are you sure you want to download all attachments?",
|
||||
context: "local",
|
||||
positiveText: "Download",
|
||||
title: strings.doActions.download.attachment(
|
||||
attachments.placeholders.length
|
||||
),
|
||||
positiveText: strings.network.download(),
|
||||
positivePress: async () => {
|
||||
downloadAttachments(await attachments.ids());
|
||||
},
|
||||
negativeText: "Cancel"
|
||||
negativeText: strings.cancel()
|
||||
});
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
@@ -414,7 +414,7 @@ export const AttachmentDialog = ({
|
||||
>
|
||||
<Seperator />
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
placeholder={strings.filterAttachments()}
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
@@ -456,14 +456,16 @@ export const AttachmentDialog = ({
|
||||
<Paragraph>
|
||||
{rechecker.isWorking
|
||||
? note
|
||||
? `Checking ${rechecker.filter.toLowerCase()} note attachments`
|
||||
: `Checking ${rechecker.filter.toLowerCase()} attachments`
|
||||
: "Attachments recheck complete"}
|
||||
? strings.checkingNoteAttachments()
|
||||
: strings.checkingAllAttachments()
|
||||
: strings.attachmentRecheckComplete()}
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
{`${rechecker.isWorking ? "Please wait... " : ""}Passed: ${
|
||||
{`${
|
||||
rechecker.isWorking ? `${strings.pleaseWait()} ` : ""
|
||||
}${strings.passed()}: ${
|
||||
rechecker.passed
|
||||
}, Failed: ${rechecker.failed}`}
|
||||
}, ${strings.failed()}: ${rechecker.failed}`}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
@@ -554,9 +556,7 @@ export const AttachmentDialog = ({
|
||||
size={60}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
<Paragraph>{strings.noAttachments()}</Paragraph>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -19,6 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
@@ -28,9 +31,12 @@ import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Toast } from "../toast";
|
||||
import { initialAuthMode } from "./common";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth, initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const AuthMode = {
|
||||
login: 0,
|
||||
@@ -44,6 +50,7 @@ const AuthModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
|
||||
const actionSheetRef = useRef();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenLoginDialog, open);
|
||||
@@ -91,18 +98,81 @@ const AuthModal = () => {
|
||||
centered={false}
|
||||
enableSheetKeyboardHandler
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
top: 0,
|
||||
zIndex: 999,
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent:
|
||||
initialAuthMode.current !== AuthMode.welcomeSignup
|
||||
? "space-between"
|
||||
: "flex-end"
|
||||
}}
|
||||
>
|
||||
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||
<Button
|
||||
title={strings.skip()}
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
|
||||
@@ -35,6 +35,7 @@ import Seperator from "../ui/seperator";
|
||||
import { Dialog } from "../dialog";
|
||||
import BackupService from "../../services/backup";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const ChangePassword = () => {
|
||||
const passwordInputRef = useRef();
|
||||
@@ -49,8 +50,8 @@ export const ChangePassword = () => {
|
||||
const changePassword = async () => {
|
||||
if (!user?.isEmailConfirmed) {
|
||||
ToastManager.show({
|
||||
heading: "Email not confirmed",
|
||||
message: "Please confirm your email to change account password",
|
||||
heading: strings.emailNotConfirmed(),
|
||||
message: strings.emailNotConfirmedDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -58,8 +59,8 @@ export const ChangePassword = () => {
|
||||
}
|
||||
if (error || !oldPassword.current || !password.current) {
|
||||
ToastManager.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again.",
|
||||
heading: strings.allFieldsRequired(),
|
||||
message: strings.allFieldsRequiredDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -68,13 +69,13 @@ export const ChangePassword = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(false, "change-password-dialog");
|
||||
if (result.error)
|
||||
throw new Error(`Failed to create backup: ${result.error}`);
|
||||
if (!result.error)
|
||||
throw new Error(strings.backupFailed() + `: ${result.error}`);
|
||||
|
||||
await db.user.clearSessions();
|
||||
await db.user.changePassword(oldPassword.current, password.current);
|
||||
ToastManager.show({
|
||||
heading: "Account password updated",
|
||||
heading: strings.passwordChangedSuccessfully(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
@@ -85,7 +86,7 @@ export const ChangePassword = () => {
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "Failed to change password",
|
||||
heading: strings.passwordChangeFailed(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -102,10 +103,7 @@ export const ChangePassword = () => {
|
||||
}}
|
||||
>
|
||||
<Dialog context="change-password-dialog" />
|
||||
<DialogHeader
|
||||
title="Change password"
|
||||
paragraph="Enter your old and new passwords"
|
||||
/>
|
||||
<DialogHeader title={strings.changePassword()} />
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
@@ -119,7 +117,7 @@ export const ChangePassword = () => {
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="Old Password"
|
||||
placeholder={strings.oldPassword()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
@@ -128,27 +126,21 @@ export const ChangePassword = () => {
|
||||
password.current = value;
|
||||
}}
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
validationType="password"
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="New password"
|
||||
placeholder={strings.newPassword()}
|
||||
/>
|
||||
|
||||
<Notice
|
||||
text={`Changing password is an irreversible process. You will be logged out from all your devices. Please make sure you do not close the app while your password is changing and have good internet connection.`}
|
||||
type="alert"
|
||||
/>
|
||||
<Notice text={strings.changePasswordNotice()} type="alert" />
|
||||
|
||||
<View style={{ height: 10 }} />
|
||||
|
||||
<Notice
|
||||
text={`Once your password is changed, please make sure to save the new account recovery key.`}
|
||||
type="alert"
|
||||
/>
|
||||
<Notice text={strings.changePasswordNotice2()} type="alert" />
|
||||
|
||||
<Button
|
||||
style={{
|
||||
@@ -158,7 +150,7 @@ export const ChangePassword = () => {
|
||||
loading={loading}
|
||||
onPress={changePassword}
|
||||
type="accent"
|
||||
title={loading ? null : "I understand, change my password"}
|
||||
title={loading ? null : strings.changePasswordConfirm()}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -32,6 +32,7 @@ import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const ForgotPassword = () => {
|
||||
const { colors } = useThemeColors("sheet");
|
||||
@@ -44,7 +45,7 @@ export const ForgotPassword = () => {
|
||||
const sendRecoveryEmail = async () => {
|
||||
if (!email.current || error) {
|
||||
ToastManager.show({
|
||||
heading: "Account email is required.",
|
||||
heading: strings.emailRequired(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -57,15 +58,15 @@ export const ForgotPassword = () => {
|
||||
lastRecoveryEmailTime &&
|
||||
Date.now() - JSON.parse(lastRecoveryEmailTime) < 60000 * 3
|
||||
) {
|
||||
throw new Error("Please wait before requesting another email");
|
||||
throw new Error(strings.pleaseWaitBeforeSendEmail());
|
||||
}
|
||||
await db.user.recoverAccount(email.current.toLowerCase());
|
||||
SettingsService.set({
|
||||
lastRecoveryEmailTime: Date.now()
|
||||
});
|
||||
ToastManager.show({
|
||||
heading: "Check your email to reset password",
|
||||
message: `Recovery email has been sent to ${email.current.toLowerCase()}`,
|
||||
heading: strings.recoveryEmailSent(),
|
||||
message: strings.recoveryEmailSentDesc(),
|
||||
type: "success",
|
||||
context: "local",
|
||||
duration: 7000
|
||||
@@ -75,7 +76,7 @@ export const ForgotPassword = () => {
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "Recovery email not sent",
|
||||
heading: strings.recoveryEmailFailed(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -121,13 +122,13 @@ export const ForgotPassword = () => {
|
||||
name="email"
|
||||
size={50}
|
||||
/>
|
||||
<Heading>Recovery email sent!</Heading>
|
||||
<Heading>{strings.recoveryEmailSent()}</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Please follow the link in the email to recover your account.
|
||||
{strings.recoveryEmailSentDesc()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : (
|
||||
@@ -140,10 +141,7 @@ export const ForgotPassword = () => {
|
||||
padding: 12
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title="Account recovery"
|
||||
paragraph="We will send you an email with steps on how to reset your password."
|
||||
/>
|
||||
<DialogHeader title={strings.accountRecovery()} />
|
||||
<Seperator />
|
||||
|
||||
<Input
|
||||
@@ -153,14 +151,14 @@ export const ForgotPassword = () => {
|
||||
}}
|
||||
defaultValue={email.current}
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Email"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
onSubmit={() => {}}
|
||||
/>
|
||||
|
||||
@@ -172,7 +170,7 @@ export const ForgotPassword = () => {
|
||||
loading={loading}
|
||||
onPress={sendRecoveryEmail}
|
||||
type="accent"
|
||||
title={loading ? null : "Next"}
|
||||
title={loading ? null : strings.next()}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth } from "./common";
|
||||
|
||||
export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent: !props.welcome ? "space-between" : "flex-end"
|
||||
}}
|
||||
>
|
||||
{props.welcome ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!props.welcome ? null : (
|
||||
<Button
|
||||
title="Skip"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -21,7 +21,6 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Sync from "../../services/sync";
|
||||
@@ -39,6 +38,7 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { useLogin } from "./use-login";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -90,233 +90,220 @@ export const Login = ({ changeMode }) => {
|
||||
<>
|
||||
<ForgotPassword />
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<KeyboardAwareScrollView
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: 12,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: 12,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor:
|
||||
deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
extraBold
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Login to your {"\n"}account
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Enter your email"
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
<Button
|
||||
title="Forgot your password?"
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
height: 30,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
SheetManager.show("forgotpassword_sheet", email.current);
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
>
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
extraBold
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
{strings.loginToYourAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.emailInvalid()}
|
||||
placeholder={strings.email()}
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
<Button
|
||||
loading={loading}
|
||||
title={strings.forgotPassword()}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
height: 30,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
SheetManager.show("forgotpassword_sheet", email.current);
|
||||
}}
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
fontSize={SIZE.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<Button
|
||||
title="Cancel logging in"
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
height: 30,
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="errorShade"
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
}}
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? strings.continue() : null}
|
||||
/>
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<Button
|
||||
title={strings.cancelLogin()}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
height: 30,
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="errorShade"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.dontHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
Don't have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
Sign up
|
||||
</Paragraph>
|
||||
{strings.signUp()}
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { LoginSteps, useLogin } from "./use-login";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
function getObfuscatedEmail(email) {
|
||||
if (!email) return "";
|
||||
@@ -193,15 +194,14 @@ export const SessionExpired = () => {
|
||||
size={50}
|
||||
/>
|
||||
<Heading size={SIZE.xxxl} color={colors.primary.heading}>
|
||||
Session expired
|
||||
{strings.sessionExpired()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Your session on this device has expired. Please enter password for{" "}
|
||||
{getObfuscatedEmail(email.current)} to continue.
|
||||
{strings.sessionExpiredDesc(getObfuscatedEmail(email.current))}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
@@ -211,13 +211,13 @@ export const SessionExpired = () => {
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
placeholder={strings.password()}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
) : null}
|
||||
@@ -231,7 +231,7 @@ export const SessionExpired = () => {
|
||||
loading={loading}
|
||||
onPress={() => login()}
|
||||
type="accent"
|
||||
title={loading ? null : "Login"}
|
||||
title={loading ? null : strings.login()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
@@ -242,16 +242,15 @@ export const SessionExpired = () => {
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
context: "session_expiry",
|
||||
title: "Logout",
|
||||
paragraph:
|
||||
"Are you sure you want to logout from this device? Any unsynced changes will be lost.",
|
||||
positiveText: "Logout",
|
||||
title: strings.logoutFromDevice(),
|
||||
paragraph: strings.logoutDesc(),
|
||||
positiveText: strings.logout(),
|
||||
positiveType: "errorShade",
|
||||
positivePress: logout
|
||||
});
|
||||
}}
|
||||
type="errorShade"
|
||||
title={loading ? null : "Logout from this device"}
|
||||
title={loading ? null : strings.logoutFromDevice()}
|
||||
/>
|
||||
</View>
|
||||
<Toast context="local" />
|
||||
|
||||
@@ -17,20 +17,18 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Loading } from "../loading";
|
||||
import { PaywallComponent } from "../premium/component";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
@@ -38,17 +36,9 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { AuthHeader } from "./header";
|
||||
import { SignupContext } from "./signup-context";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
selectPlan: 1,
|
||||
createAccount: 2
|
||||
};
|
||||
|
||||
export const Signup = ({ changeMode, welcome }) => {
|
||||
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const email = useRef();
|
||||
const emailInputRef = useRef();
|
||||
@@ -61,12 +51,12 @@ export const Signup = ({ changeMode, welcome }) => {
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const { height } = useWindowDimensions();
|
||||
const { width, height } = useWindowDimensions();
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastManager.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again",
|
||||
heading: strings.allFieldsRequired(),
|
||||
message: strings.allFieldsRequiredDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -80,287 +70,238 @@ export const Signup = ({ changeMode, welcome }) => {
|
||||
const signup = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
if (loading) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
await db.user.signup(email.current.toLowerCase(), password.current);
|
||||
let user = await db.user.getUser();
|
||||
setUser(user);
|
||||
setLastSynced(await db.lastSynced());
|
||||
clearMessage();
|
||||
setEmailVerifyMessage();
|
||||
setCurrentStep(SignupSteps.selectPlan);
|
||||
return true;
|
||||
hideAuth();
|
||||
SettingsService.setProperty("encryptedBackup", true);
|
||||
await sleep(300);
|
||||
if (trial) {
|
||||
PremiumService.sheet(null, null, true);
|
||||
} else {
|
||||
PremiumService.showVerifyEmailDialog();
|
||||
}
|
||||
} catch (e) {
|
||||
setCurrentStep(SignupSteps.signup);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "Signup failed",
|
||||
heading: strings.signupFailed(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SignupContext.Provider
|
||||
value={{
|
||||
signup: signup
|
||||
}}
|
||||
>
|
||||
{currentStep === SignupSteps.signup ? (
|
||||
<>
|
||||
<AuthHeader welcome={welcome} />
|
||||
<KeyboardAwareScrollView
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
flexDirection: "row"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%"
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
{strings.createYourAccount()}
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage={strings.email()}
|
||||
placeholder={strings.email()}
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
validationType="password"
|
||||
autoCorrect={false}
|
||||
placeholder={strings.password()}
|
||||
onSubmit={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder={strings.confirmPassword()}
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{strings.signupAgreement[0]()}
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor:
|
||||
deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.25
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
{strings.signupAgreement[1]()}
|
||||
</Paragraph>
|
||||
{strings.signupAgreement[2]()}
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
{strings.signupAgreement[3]()}
|
||||
</Paragraph>
|
||||
{strings.signupAgreement[4]()}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
extraBold
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Create your account
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: colors.primary.background,
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Email"
|
||||
blurOnSubmit={false}
|
||||
onSubmit={() => {
|
||||
if (!email.current) return;
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
blurOnSubmit={false}
|
||||
validationType="password"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
onSubmit={() => {
|
||||
if (!password.current) return;
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Signup"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder="Confirm password"
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
signup();
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
Already have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
Login
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Button
|
||||
title={!loading ? strings.continue() : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={signup}
|
||||
fontSize={SIZE.md}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.secondary.paragraph}>
|
||||
{strings.alreadyHaveAccount()}{" "}
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
paddingHorizontal: 16,
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
By signing up, you agree to our{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Terms of Service{" "}
|
||||
</Paragraph>
|
||||
and{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Privacy Policy.
|
||||
</Paragraph>{" "}
|
||||
You also agree to recieve marketing emails from us which you can
|
||||
opt-out of from app settings.
|
||||
{strings.login()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
) : currentStep === SignupSteps.createAccount ? (
|
||||
<>
|
||||
<Loading
|
||||
title="Setting up your account..."
|
||||
description="Your account is almost ready, please wait..."
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PaywallComponent
|
||||
close={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
setupAccount={() => {
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
}}
|
||||
isModal={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SignupContext.Provider>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,8 +17,11 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
import { db } from "../../common/database/index";
|
||||
import useTimer from "../../hooks/use-timer";
|
||||
import {
|
||||
@@ -26,20 +29,16 @@ import {
|
||||
presentSheet,
|
||||
ToastManager
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Input from "../ui/input";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
|
||||
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const code = useRef();
|
||||
const [currentMethod, setCurrentMethod] = useState({
|
||||
@@ -51,20 +50,6 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
const inputRef = useRef();
|
||||
const [sending, setSending] = useState(false);
|
||||
|
||||
const codeHelpText = {
|
||||
app: "Enter the 6 digit code from your authenticator app to continue logging in",
|
||||
sms: "Enter the 6 digit code sent to your phone number to continue logging in",
|
||||
email: "Enter the 6 digit code sent to your email to continue logging in",
|
||||
recoveryCode: "Enter the recovery code to continue logging in"
|
||||
};
|
||||
|
||||
const secondaryMethodsText = {
|
||||
app: "I don't have access to authenticator app",
|
||||
sms: "I don't have access to my phone",
|
||||
email: "I don't have access to email",
|
||||
recoveryCode: "I don't have recovery codes"
|
||||
};
|
||||
|
||||
const onNext = async () => {
|
||||
if (!code.current || code.current.length < 6) return;
|
||||
setLoading(true);
|
||||
@@ -94,22 +79,22 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
const methods = [
|
||||
{
|
||||
id: "sms",
|
||||
title: "Send code via SMS",
|
||||
title: strings.sendCodeSms(),
|
||||
icon: "message-plus-outline"
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
title: "Send code via email",
|
||||
title: strings.sendCodeEmail(),
|
||||
icon: "email-outline"
|
||||
},
|
||||
{
|
||||
id: "app",
|
||||
title: "Enter code from authenticator app",
|
||||
title: strings.authAppCode(),
|
||||
icon: "cellphone-key"
|
||||
},
|
||||
{
|
||||
id: "recoveryCode",
|
||||
title: "I have a recovery code",
|
||||
title: strings.recoveryCode(),
|
||||
icon: "key"
|
||||
}
|
||||
];
|
||||
@@ -151,7 +136,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
paddingHorizontal: currentMethod.method ? 12 : 0
|
||||
paddingHorizontal: currentMethod.method ? 12 : 0,
|
||||
gap: 12
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
@@ -168,9 +154,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{currentMethod.method
|
||||
? "Two factor authentication"
|
||||
: "Select methods for two-factor authentication"}
|
||||
{currentMethod.method ? strings["2fa"]() : strings.select2faMethod()}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
@@ -178,12 +162,10 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{codeHelpText[currentMethod.method] ||
|
||||
"Select how you would like to recieve the code"}
|
||||
{strings["2faCodeHelpText"][currentMethod.method]?.() ||
|
||||
strings.select2faCodeHelpText()}
|
||||
</Paragraph>
|
||||
|
||||
<Seperator />
|
||||
|
||||
{currentMethod.method === "sms" || currentMethod.method === "email" ? (
|
||||
<Button
|
||||
onPress={onSendCode}
|
||||
@@ -191,15 +173,17 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
title={
|
||||
sending
|
||||
? ""
|
||||
: `${seconds ? `Resend code in (${seconds})` : "Send code"}`
|
||||
: `${
|
||||
seconds
|
||||
? strings.resend2faCode(seconds)
|
||||
: strings.sendCode()
|
||||
}`
|
||||
}
|
||||
loading={sending}
|
||||
height={30}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Seperator />
|
||||
|
||||
{currentMethod.method ? (
|
||||
<>
|
||||
<Input
|
||||
@@ -238,25 +222,33 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
minWidth: "50%"
|
||||
}}
|
||||
/>
|
||||
<Seperator />
|
||||
|
||||
<Button
|
||||
title={loading ? null : "Next"}
|
||||
title={loading ? null : strings.next()}
|
||||
type="accent"
|
||||
width={250}
|
||||
loading={loading}
|
||||
onPress={onNext}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
marginBottom: 10
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={secondaryMethodsText[currentMethod.method]}
|
||||
title={strings["2faCodeSecondaryMethodText"][
|
||||
currentMethod.method
|
||||
]()}
|
||||
type="plain"
|
||||
onPress={onRequestSecondaryMethod}
|
||||
height={30}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={strings.cancel()}
|
||||
type="plain"
|
||||
onPress={onCancel}
|
||||
height={30}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@@ -307,15 +299,16 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
);
|
||||
};
|
||||
|
||||
TwoFactorVerification.present = (onMfaLogin, data, context) => {
|
||||
TwoFactorVerification.present = (onMfaLogin, data, onCancel, context) => {
|
||||
presentSheet({
|
||||
component: () => (
|
||||
<TwoFactorVerification onMfaLogin={onMfaLogin} mfaInfo={data} />
|
||||
<TwoFactorVerification
|
||||
onMfaLogin={onMfaLogin}
|
||||
mfaInfo={data}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
),
|
||||
context: context || "two_factor_verify",
|
||||
onClose: () => {
|
||||
onMfaLogin();
|
||||
},
|
||||
disableClosing: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,15 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { TextInput } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eCloseSheet } from "../../utils/events";
|
||||
import TwoFactorVerification from "./two-factor";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -34,18 +34,15 @@ export const LoginSteps = {
|
||||
passwordAuth: 3
|
||||
};
|
||||
|
||||
export const useLogin = (
|
||||
onFinishLogin?: () => void,
|
||||
sessionExpired = false
|
||||
) => {
|
||||
export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const [step, setStep] = useState(LoginSteps.emailAuth);
|
||||
const email = useRef<string>();
|
||||
const password = useRef<string>();
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const email = useRef();
|
||||
const password = useRef();
|
||||
const emailInputRef = useRef();
|
||||
const passwordInputRef = useRef();
|
||||
|
||||
const validateInfo = () => {
|
||||
if (
|
||||
@@ -53,8 +50,8 @@ export const useLogin = (
|
||||
(!email.current && step === LoginSteps.emailAuth)
|
||||
) {
|
||||
ToastManager.show({
|
||||
heading: "All fields required",
|
||||
message: "Fill all the fields and try again",
|
||||
heading: strings.allFieldsRequired(),
|
||||
message: strings.allFieldsRequiredDesc(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -72,17 +69,11 @@ export const useLogin = (
|
||||
setLoading(true);
|
||||
switch (step) {
|
||||
case LoginSteps.emailAuth: {
|
||||
if (!email.current) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
const mfaInfo = await db.user.authenticateEmail(email.current);
|
||||
console.log("email auth", mfaInfo);
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(
|
||||
async (
|
||||
mfa: { code: string; method: string },
|
||||
callback: (value: boolean) => void
|
||||
) => {
|
||||
async (mfa, callback) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
@@ -94,35 +85,36 @@ export const useLogin = (
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 1);
|
||||
}, 500);
|
||||
callback && callback(true);
|
||||
}
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if ((e as Error).message === "invalid_grant") {
|
||||
if (e.message === "invalid_grant") {
|
||||
eSendEvent(eCloseSheet, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
}
|
||||
},
|
||||
mfaInfo
|
||||
mfaInfo,
|
||||
() => {
|
||||
eSendEvent(eCloseSheet, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
finishWithError(new Error("Unable to send 2FA code"));
|
||||
finishWithError(new Error(strings.unableToSend2faCode()));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
if (!email.current || !password.current) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
await db.user.authenticatePassword(
|
||||
email.current,
|
||||
password.current,
|
||||
undefined,
|
||||
null,
|
||||
sessionExpired
|
||||
);
|
||||
finishLogin();
|
||||
@@ -131,15 +123,15 @@ export const useLogin = (
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
finishWithError(e as Error);
|
||||
finishWithError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const finishWithError = async (e: Error) => {
|
||||
const finishWithError = async (e) => {
|
||||
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "Login failed",
|
||||
heading: strings.loginFailed(),
|
||||
message: e.message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
@@ -148,13 +140,13 @@ export const useLogin = (
|
||||
|
||||
const finishLogin = async () => {
|
||||
const user = await db.user.getUser();
|
||||
if (!user) throw new Error("Email or password incorrect!");
|
||||
if (!user) throw new Error(strings.emailOrPasswordIncorrect());
|
||||
PremiumService.setPremiumStatus();
|
||||
setUser(user);
|
||||
clearMessage();
|
||||
ToastManager.show({
|
||||
heading: "Login successful",
|
||||
message: `Logged in as ${user.email}`,
|
||||
heading: strings.loginSuccess(),
|
||||
message: strings.loginSuccessDesc(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
@@ -34,11 +34,9 @@ import { useSelectionStore } from "../../stores/use-selection-store";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { SIZE, normalize } from "../../utils/size";
|
||||
import NativeTooltip from "../../utils/tooltip";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
|
||||
interface FloatingButtonProps {
|
||||
title: string;
|
||||
onPress: () => void;
|
||||
color?: string;
|
||||
shouldShow?: boolean;
|
||||
@@ -46,7 +44,6 @@ interface FloatingButtonProps {
|
||||
}
|
||||
|
||||
const FloatingButton = ({
|
||||
title,
|
||||
onPress,
|
||||
color,
|
||||
alwaysVisible = false
|
||||
@@ -126,11 +123,6 @@ const FloatingButton = ({
|
||||
...getElevationStyle(5),
|
||||
borderRadius: 100
|
||||
}}
|
||||
onLongPress={(event) => {
|
||||
if (title) {
|
||||
NativeTooltip.show(event, title, NativeTooltip.POSITIONS.LEFT);
|
||||
}
|
||||
}}
|
||||
onPress={onPress}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -26,12 +26,13 @@ import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getColorLinearShade } from "../../utils/colors";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const DialogButtons = ({
|
||||
onPressPositive,
|
||||
onPressNegative,
|
||||
positiveTitle,
|
||||
negativeTitle = "Cancel",
|
||||
negativeTitle = strings.cancel(),
|
||||
loading,
|
||||
doneText,
|
||||
positiveType
|
||||
|
||||
@@ -38,6 +38,7 @@ import { useCallback } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
import { Notice } from "../ui/notice";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -50,8 +51,8 @@ export const Dialog = ({ context = "global" }) => {
|
||||
const [dialogInfo, setDialogInfo] = useState({
|
||||
title: "",
|
||||
paragraph: "",
|
||||
positiveText: "Done",
|
||||
negativeText: "Cancel",
|
||||
positiveText: strings.done(),
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: () => {},
|
||||
onClose: () => {},
|
||||
positiveType: "transparent",
|
||||
|
||||
@@ -49,6 +49,7 @@ import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const AppLockPassword = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -119,22 +120,7 @@ export const AppLockPassword = () => {
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={
|
||||
mode === "change"
|
||||
? `Change app lock ${keyboardType}`
|
||||
: mode === "remove"
|
||||
? `Remove app lock ${keyboardType}`
|
||||
: `Set up app lock ${keyboardType}`
|
||||
}
|
||||
paragraph={
|
||||
mode === "change"
|
||||
? `Change app lock ${keyboardType}`
|
||||
: mode === "remove"
|
||||
? `Enter ${
|
||||
accountPass ? "account password" : `app lock ${keyboardType}`
|
||||
} to remove ${keyboardType}`
|
||||
: `Set up a custom app lock ${keyboardType} to unlock the app`
|
||||
}
|
||||
title={strings.changeAppLockCredentials(mode, keyboardType)}
|
||||
icon="shield"
|
||||
padding={12}
|
||||
/>
|
||||
@@ -157,11 +143,15 @@ export const AppLockPassword = () => {
|
||||
}}
|
||||
defaultValue={values.current.currentPassword}
|
||||
autoComplete="password"
|
||||
returnKeyLabel="Next"
|
||||
returnKeyLabel={strings.next()}
|
||||
keyboardType={keyboardType === "pin" ? "number-pad" : "default"}
|
||||
returnKeyType="next"
|
||||
secureTextEntry={secureTextEntry}
|
||||
placeholder={`Current ${keyboardType}`}
|
||||
placeholder={
|
||||
keyboardType === "pin"
|
||||
? strings.currentPin()
|
||||
: strings.currentPassword()
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -179,7 +169,9 @@ export const AppLockPassword = () => {
|
||||
keyboardType === "pin" && !accountPass ? "number-pad" : "default"
|
||||
}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={mode !== "remove" ? "Next" : "Remove"}
|
||||
returnKeyLabel={
|
||||
mode !== "remove" ? strings.next() : strings.remove()
|
||||
}
|
||||
returnKeyType={mode !== "remove" ? "next" : "done"}
|
||||
secureTextEntry={secureTextEntry}
|
||||
buttonLeft={
|
||||
@@ -206,10 +198,14 @@ export const AppLockPassword = () => {
|
||||
}
|
||||
placeholder={
|
||||
accountPass
|
||||
? "Account password"
|
||||
? strings.enterAccountPassword()
|
||||
: mode === "change"
|
||||
? `New ${keyboardType}`
|
||||
: `${keyboardType === "pin" ? "Pin" : "Password"}`
|
||||
? keyboardType === "pin"
|
||||
? strings.newPin()
|
||||
: strings.newPassword()
|
||||
: `${
|
||||
keyboardType === "pin" ? strings.pin() : strings.password()
|
||||
}`
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -228,10 +224,14 @@ export const AppLockPassword = () => {
|
||||
customValidator={() => values.current.password || ""}
|
||||
validationType="confirmPassword"
|
||||
autoComplete="password"
|
||||
returnKeyLabel="Done"
|
||||
returnKeyLabel={strings.done()}
|
||||
returnKeyType="done"
|
||||
secureTextEntry={secureTextEntry}
|
||||
placeholder={`Confirm ${keyboardType}`}
|
||||
placeholder={
|
||||
keyboardType === "pin"
|
||||
? strings.confirmPin()
|
||||
: strings.confirmPassword()
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -253,7 +253,7 @@ export const AppLockPassword = () => {
|
||||
iconColor={
|
||||
accountPass ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
title="Use account password instead"
|
||||
title={strings.useAccountPassword()}
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "flex-start",
|
||||
@@ -272,7 +272,7 @@ export const AppLockPassword = () => {
|
||||
if (mode === "create") {
|
||||
if (!values.current.password || !values.current.confirmPassword) {
|
||||
ToastManager.error(
|
||||
new Error("All inputs are required"),
|
||||
new Error(strings.allFieldsRequired()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
@@ -281,11 +281,7 @@ export const AppLockPassword = () => {
|
||||
|
||||
if (values.current.password !== values.current.confirmPassword) {
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
`${
|
||||
keyboardType === "pin" ? "Pin" : "Password"
|
||||
} does not match`
|
||||
),
|
||||
new Error(strings.mismatch(keyboardType)),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
@@ -301,7 +297,7 @@ export const AppLockPassword = () => {
|
||||
!values.current.confirmPassword
|
||||
) {
|
||||
ToastManager.error(
|
||||
new Error("All inputs are required"),
|
||||
new Error(strings.allFieldsRequired()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
@@ -310,11 +306,7 @@ export const AppLockPassword = () => {
|
||||
|
||||
if (values.current.password !== values.current.confirmPassword) {
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
`${
|
||||
keyboardType === "pin" ? "Pin" : "Password"
|
||||
} does not match`
|
||||
),
|
||||
new Error(strings.mismatch(keyboardType)),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
@@ -326,9 +318,7 @@ export const AppLockPassword = () => {
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
`${keyboardType === "pin" ? "Pin" : "Password"} incorrect`
|
||||
),
|
||||
new Error(strings.incorrect(keyboardType)),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
@@ -342,7 +332,7 @@ export const AppLockPassword = () => {
|
||||
} else if (mode === "remove") {
|
||||
if (!values.current.password) {
|
||||
ToastManager.error(
|
||||
new Error("All inputs are required"),
|
||||
new Error(strings.allFieldsRequired()),
|
||||
undefined,
|
||||
"local"
|
||||
);
|
||||
@@ -357,10 +347,8 @@ export const AppLockPassword = () => {
|
||||
ToastManager.error(
|
||||
new Error(
|
||||
accountPass
|
||||
? "Account password incorrect"
|
||||
: `${
|
||||
keyboardType === "pin" ? "Pin" : "Password"
|
||||
} incorrect`
|
||||
? strings.passwordIncorrect()
|
||||
: strings.incorrect(keyboardType)
|
||||
),
|
||||
undefined,
|
||||
"local"
|
||||
@@ -377,7 +365,7 @@ export const AppLockPassword = () => {
|
||||
SettingsService.setProperty("appLockEnabled", false);
|
||||
SettingsService.setPrivacyScreen(SettingsService.get());
|
||||
ToastManager.show({
|
||||
message: "App lock disabled",
|
||||
message: strings.applockDisabled(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
@@ -391,9 +379,13 @@ export const AppLockPassword = () => {
|
||||
close();
|
||||
}}
|
||||
positiveTitle={
|
||||
mode === "remove" ? "Remove" : mode === "change" ? "Change" : "Save"
|
||||
mode === "remove"
|
||||
? strings.remove()
|
||||
: mode === "change"
|
||||
? strings.change()
|
||||
: strings.save()
|
||||
}
|
||||
negativeTitle="Cancel"
|
||||
negativeTitle={strings.cancel()}
|
||||
positiveType="transparent"
|
||||
loading={false}
|
||||
doneText=""
|
||||
|
||||
@@ -25,13 +25,14 @@ import {
|
||||
View
|
||||
} from "react-native";
|
||||
import { Image as ImageType } from "react-native-image-crop-picker";
|
||||
import { useThemeColors } from "../../../../../../packages/theme/dist";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import { Notice } from "../../ui/notice";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export default function AttachImage({
|
||||
response,
|
||||
@@ -64,7 +65,7 @@ export default function AttachImage({
|
||||
}}
|
||||
>
|
||||
<Paragraph style={{ color: colors.primary.paragraph, marginBottom: 6 }}>
|
||||
Attaching {response?.length} image(s):
|
||||
{strings.attachImageHeading(response?.length || 1)}
|
||||
</Paragraph>
|
||||
<ScrollView horizontal>
|
||||
{response?.map?.((item) => (
|
||||
@@ -123,14 +124,14 @@ export default function AttachImage({
|
||||
}}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
Compress (recommended)
|
||||
{strings.compress()} ({strings.recommended().toLowerCase()})
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
|
||||
{!compress ? (
|
||||
<Notice
|
||||
type="alert"
|
||||
text="Images uploaded without compression are slow to load and take more bandwidth. We recommend compressing images unless you need image in original quality."
|
||||
text={strings.compressionOffNotice()}
|
||||
size="small"
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -140,7 +141,7 @@ export default function AttachImage({
|
||||
) : (
|
||||
<Notice
|
||||
type="information"
|
||||
text="Compressed images are uploaded in Full HD resolution and usually are good enough for most use cases."
|
||||
text={strings.compressionOnNotice()}
|
||||
size="small"
|
||||
style={{
|
||||
width: "100%",
|
||||
@@ -150,9 +151,7 @@ export default function AttachImage({
|
||||
)}
|
||||
|
||||
<Button
|
||||
title={`${
|
||||
(response?.length || 0) > 1 ? "Attach Images" : "Attach Image"
|
||||
}`}
|
||||
title={strings.attachImageHeading(response?.length || 1)}
|
||||
type="accent"
|
||||
width="100%"
|
||||
onPress={() => {
|
||||
|
||||
@@ -33,6 +33,7 @@ import { Toast } from "../../toast";
|
||||
import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const HEX_COLOR_REGEX_ALPHA =
|
||||
/^#(?:(?:[\da-fA-F]{3}){1,2}|(?:[\da-fA-F]{4}){1,2})$/;
|
||||
@@ -148,31 +149,31 @@ const ColorPicker = ({
|
||||
}}
|
||||
testID="color-title-input"
|
||||
defaultValue={title.current}
|
||||
placeholder={title.current || "Color title"}
|
||||
placeholder={title.current || strings.colorTitle()}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title="Add color"
|
||||
title={strings.addColor()}
|
||||
style={{
|
||||
marginBottom: 10
|
||||
}}
|
||||
onPress={async () => {
|
||||
if (!selectedColor)
|
||||
return ToastManager.error(
|
||||
new Error("Select a color"),
|
||||
new Error(strings.noColorSelected()),
|
||||
undefined,
|
||||
"color-picker"
|
||||
);
|
||||
if (!title.current)
|
||||
return ToastManager.error(
|
||||
new Error("Enter a title for the color")
|
||||
new Error(strings.allFieldsRequired())
|
||||
);
|
||||
const exists = await db.colors.all.find((v) =>
|
||||
v.and([v(`colorCode`, "==", selectedColor)])
|
||||
);
|
||||
if (exists)
|
||||
return ToastManager.error(
|
||||
new Error(`Color #${selectedColor} already exists`)
|
||||
new Error(strings.colorExists(selectedColor))
|
||||
);
|
||||
const id = await db.colors.add({
|
||||
title: title.current,
|
||||
|
||||
@@ -45,6 +45,7 @@ import Paragraph from "../../ui/typography/paragraph";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import { deleteCacheFileByPath } from "../../../common/filesystem/io";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const WIN_WIDTH = Dimensions.get("window").width;
|
||||
const WIN_HEIGHT = Dimensions.get("window").height;
|
||||
@@ -137,10 +138,10 @@ const PDFPreview = () => {
|
||||
presentDialog({
|
||||
context: attachment?.hash,
|
||||
input: true,
|
||||
inputPlaceholder: "Enter password",
|
||||
positiveText: "Unlock",
|
||||
title: "Decrypt",
|
||||
paragraph: "Please input password to view pdf.",
|
||||
inputPlaceholder: strings.enterPassword(),
|
||||
positiveText: strings.open(),
|
||||
title: strings.pdfLocked(),
|
||||
paragraph: strings.pdfLockedDesc(),
|
||||
positivePress: (value) => {
|
||||
setTimeout(() => {
|
||||
setPassword(value);
|
||||
@@ -187,8 +188,7 @@ const PDFPreview = () => {
|
||||
}}
|
||||
color={colors.static.white}
|
||||
>
|
||||
Loading {`${progress?.percent ? `(${progress?.percent})` : ""}`}
|
||||
... Please wait
|
||||
{strings.loadingWithProgress(progress.percent)}
|
||||
</Paragraph>
|
||||
</Animated.View>
|
||||
) : (
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function Progress() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
{data?.canHideProgress ? null : (
|
||||
{!data?.canHideProgress ? null : (
|
||||
<Button
|
||||
title={cancelCallback.current ? "Cancel" : "Hide"}
|
||||
type="secondaryAccented"
|
||||
|
||||
@@ -39,10 +39,9 @@ const ResultDialog = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [dialogData, setDialogData] = useState({
|
||||
title: "Thank you for signing up!",
|
||||
paragraph:
|
||||
"Try out all features of Notesnook free for 7 days. No limitations. No commitments.",
|
||||
button: "Start taking notes"
|
||||
title: "",
|
||||
paragraph: "",
|
||||
button: ""
|
||||
});
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenResultDialog, open);
|
||||
|
||||
@@ -52,6 +52,7 @@ import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export class VaultDialog extends Component {
|
||||
constructor(props) {
|
||||
@@ -76,7 +77,7 @@ export class VaultDialog extends Component {
|
||||
changePassword: false,
|
||||
copyNote: false,
|
||||
revokeFingerprintAccess: false,
|
||||
title: "Unlock Note",
|
||||
title: strings.goToEditor(),
|
||||
description: null,
|
||||
clearVault: false,
|
||||
deleteVault: false,
|
||||
@@ -91,46 +92,25 @@ export class VaultDialog extends Component {
|
||||
this.password = null;
|
||||
this.confirmPassword = null;
|
||||
this.newPassword = null;
|
||||
(this.title = !this.state.novault
|
||||
? "Create Vault"
|
||||
this.title = !this.state.novault
|
||||
? strings.createVault()
|
||||
: this.state.fingerprintAccess
|
||||
? "Vault Fingerprint Unlock"
|
||||
? strings.vaultFingerprintUnlock()
|
||||
: this.state.revokeFingerprintAccess
|
||||
? "Revoke Vault Fingerprint Unlock"
|
||||
? strings.revokeVaultFingerprintUnlock()
|
||||
: this.state.changePassword
|
||||
? "Change Vault Password"
|
||||
? strings.changeVaultPassword()
|
||||
: this.state.noteLocked
|
||||
? this.state.deleteNote
|
||||
? "Delete note"
|
||||
? strings.deleteNote()
|
||||
: this.state.share
|
||||
? "Share note"
|
||||
? strings.shareNote()
|
||||
: this.state.copyNote
|
||||
? "Copy note"
|
||||
? strings.copyNote()
|
||||
: this.state.goToEditor
|
||||
? "Unlock note"
|
||||
: "Unlock note"
|
||||
: "Lock note"),
|
||||
(this.description = !this.state.novault
|
||||
? "Set a password to create vault"
|
||||
: this.state.fingerprintAccess
|
||||
? "Enter vault password to enable fingerprint unlocking."
|
||||
: this.state.revokeFingerprintAccess
|
||||
? "Disable vault fingerprint unlock "
|
||||
: this.state.changePassword
|
||||
? "Setup a new password for the vault."
|
||||
: this.state.permanant
|
||||
? "Enter password to remove note from vault."
|
||||
: this.state.noteLocked
|
||||
? this.state.deleteNote
|
||||
? "Unlock note to delete it. If biometrics are not working, you can enter device pin to unlock vault."
|
||||
: this.state.share
|
||||
? "Unlock note to share it. If biometrics are not working, you can enter device pin to unlock vault."
|
||||
: this.state.copyNote
|
||||
? "Unlock note to copy it. If biometrics are not working, you can enter device pin to unlock vault."
|
||||
: this.state.goToEditor
|
||||
? "Unlock note to open it in editor."
|
||||
: "Enter vault password to unlock note. If biometrics are not working, you can enter device pin to unlock vault."
|
||||
: "Enter vault password to lock note. If biometrics are not working, you can enter device pin to lock note.");
|
||||
? strings.goToEditor()
|
||||
: strings.goToEditor()
|
||||
: strings.lockNote();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -199,7 +179,7 @@ export class VaultDialog extends Component {
|
||||
if (this.state.loading) {
|
||||
ToastManager.show({
|
||||
heading: this.state.title,
|
||||
message: "Please wait and do not close the app.",
|
||||
message: strings.pleaseWait() + "...",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
@@ -234,8 +214,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
if (!this.password) {
|
||||
ToastManager.show({
|
||||
heading: "Password not entered",
|
||||
message: "Enter a password for the vault and try again.",
|
||||
heading: strings.passwordNotEntered(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -245,7 +224,7 @@ export class VaultDialog extends Component {
|
||||
if (!this.state.novault) {
|
||||
if (this.password !== this.confirmPassword) {
|
||||
ToastManager.show({
|
||||
heading: "Passwords do not match",
|
||||
heading: strings.passwordNotMatched(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -271,7 +250,7 @@ export class VaultDialog extends Component {
|
||||
this._enrollFingerprint(this.newPassword);
|
||||
}
|
||||
ToastManager.show({
|
||||
heading: "Vault password updated successfully",
|
||||
heading: strings.passwordUpdated(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
@@ -283,8 +262,7 @@ export class VaultDialog extends Component {
|
||||
});
|
||||
if (e.message === db.vault.ERRORS.wrongPassword) {
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
message: "Please enter the correct password and try again",
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -295,8 +273,7 @@ export class VaultDialog extends Component {
|
||||
} else if (this.state.locked) {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
message: "Please enter the correct password and try again",
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -334,8 +311,10 @@ export class VaultDialog extends Component {
|
||||
loading: true
|
||||
});
|
||||
try {
|
||||
let verified = await db.user.verifyPassword(this.password);
|
||||
if (!(await db.user.getUser())) verified = true;
|
||||
let verified = true;
|
||||
if (await db.user.getUser()) {
|
||||
verified = await db.user.verifyPassword(this.password);
|
||||
}
|
||||
if (verified) {
|
||||
let noteIds = [];
|
||||
if (this.state.deleteAll) {
|
||||
@@ -343,20 +322,21 @@ export class VaultDialog extends Component {
|
||||
const relations = await db.relations.from(vault, "note").get();
|
||||
noteIds = relations.map((item) => item.toId);
|
||||
}
|
||||
|
||||
await db.vault.delete(this.state.deleteAll);
|
||||
|
||||
noteIds.forEach((id) => {
|
||||
eSendEvent(
|
||||
eUpdateNoteInEditor,
|
||||
{
|
||||
id: id,
|
||||
deleted: true
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
if (this.state.deleteAll) {
|
||||
noteIds.forEach((id) => {
|
||||
eSendEvent(
|
||||
eUpdateNoteInEditor,
|
||||
{
|
||||
id: id,
|
||||
deleted: true
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
}
|
||||
console.log("VAULT UPDATED EVENT");
|
||||
eSendEvent("vaultUpdated");
|
||||
this.setState({
|
||||
loading: false
|
||||
@@ -364,8 +344,7 @@ export class VaultDialog extends Component {
|
||||
this.close();
|
||||
} else {
|
||||
ToastManager.show({
|
||||
heading: "Account password incorrect",
|
||||
message: "Please enter correct password for your account.",
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -406,8 +385,7 @@ export class VaultDialog extends Component {
|
||||
eSendEvent("vaultUpdated");
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
heading: "Vault password incorrect",
|
||||
message: "Please enter correct password to clear vault.",
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -420,7 +398,7 @@ export class VaultDialog extends Component {
|
||||
async _lockNote() {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -433,7 +411,7 @@ export class VaultDialog extends Component {
|
||||
|
||||
this.close();
|
||||
ToastManager.show({
|
||||
message: "Note locked successfully",
|
||||
message: strings.noteLocked(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -446,8 +424,7 @@ export class VaultDialog extends Component {
|
||||
async _unlockNote() {
|
||||
if (!this.password || this.password.trim() === 0) {
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
message: "Please enter the correct password and try again",
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -504,8 +481,7 @@ export class VaultDialog extends Component {
|
||||
});
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastManager.show({
|
||||
heading: "Biometric unlocking enabled!",
|
||||
message: "Now you can unlock notes in vault with biometrics.",
|
||||
heading: strings.biometricUnlockEnabled(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
@@ -513,9 +489,7 @@ export class VaultDialog extends Component {
|
||||
} catch (e) {
|
||||
this.close();
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
message:
|
||||
"Please enter the correct vault password to enable biometrics.",
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -540,20 +514,20 @@ export class VaultDialog extends Component {
|
||||
loading: false
|
||||
});
|
||||
ToastManager.show({
|
||||
heading: "Note added to vault",
|
||||
heading: strings.noteLocked(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
} else {
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastManager.show({
|
||||
heading: "Vault created successfully",
|
||||
heading: strings.vaultCreated(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
eSendEvent("vaultUpdated");
|
||||
}
|
||||
|
||||
_permanantUnlock() {
|
||||
@@ -561,7 +535,7 @@ export class VaultDialog extends Component {
|
||||
.remove(this.state.note.id, this.password)
|
||||
.then(() => {
|
||||
ToastManager.show({
|
||||
heading: "Note permanently unlocked.",
|
||||
heading: strings.noteUnlocked(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
@@ -569,6 +543,7 @@ export class VaultDialog extends Component {
|
||||
this.close();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log("Error", e);
|
||||
this._takeErrorAction(e);
|
||||
});
|
||||
}
|
||||
@@ -588,9 +563,8 @@ export class VaultDialog extends Component {
|
||||
async _copyNote(note) {
|
||||
Clipboard.setString((await convertNoteToText(note, true)) || "");
|
||||
ToastManager.show({
|
||||
heading: "Note copied",
|
||||
heading: strings.noteCopied(),
|
||||
type: "success",
|
||||
message: "Note has been copied to clipboard!",
|
||||
context: "global"
|
||||
});
|
||||
this.close();
|
||||
@@ -600,7 +574,7 @@ export class VaultDialog extends Component {
|
||||
this.close();
|
||||
try {
|
||||
await Share.open({
|
||||
heading: "Share note",
|
||||
heading: note.title,
|
||||
failOnCancel: false,
|
||||
message: (await convertNoteToText(note)) || ""
|
||||
});
|
||||
@@ -610,24 +584,17 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
|
||||
_takeErrorAction(e) {
|
||||
if (
|
||||
e.message === db.vault.ERRORS.wrongPassword ||
|
||||
e.message === "FAILURE"
|
||||
) {
|
||||
this.setState({
|
||||
wrongPassword: true,
|
||||
visible: true
|
||||
this.setState({
|
||||
wrongPassword: true,
|
||||
visible: true
|
||||
});
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
heading: strings.passwordIncorrect(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
setTimeout(() => {
|
||||
ToastManager.show({
|
||||
heading: "Incorrect password",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return;
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
_revokeFingerprintAccess = async () => {
|
||||
@@ -635,14 +602,13 @@ export class VaultDialog extends Component {
|
||||
await BiometricService.resetCredentials();
|
||||
eSendEvent("vaultUpdated");
|
||||
ToastManager.show({
|
||||
heading: "Biometric unlocking disabled!",
|
||||
heading: strings.biometricUnlockDisabled(),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} catch (e) {
|
||||
ToastManager.show({
|
||||
heading: "Failed to disable Biometric unlocking.",
|
||||
message: e.message,
|
||||
heading: e.message,
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
@@ -708,12 +674,7 @@ export class VaultDialog extends Component {
|
||||
paddingTop: 12
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={this.state.title}
|
||||
paragraph={this.state.description}
|
||||
icon="shield"
|
||||
padding={12}
|
||||
/>
|
||||
<DialogHeader title={this.state.title} icon="shield" padding={12} />
|
||||
<Seperator half />
|
||||
|
||||
<View
|
||||
@@ -749,10 +710,16 @@ export class VaultDialog extends Component {
|
||||
: this.onPress;
|
||||
}}
|
||||
autoComplete="password"
|
||||
returnKeyLabel={changePassword ? "Next" : this.state.title}
|
||||
returnKeyLabel={
|
||||
changePassword ? strings.next() : this.state.title
|
||||
}
|
||||
returnKeyType={changePassword ? "next" : "done"}
|
||||
secureTextEntry
|
||||
placeholder={changePassword ? "Current password" : "Password"}
|
||||
placeholder={
|
||||
changePassword
|
||||
? strings.currentPassword()
|
||||
: strings.password()
|
||||
}
|
||||
/>
|
||||
|
||||
{!this.state.biometricUnlock ||
|
||||
@@ -761,11 +728,11 @@ export class VaultDialog extends Component {
|
||||
changePassword ? null : (
|
||||
<Button
|
||||
onPress={() =>
|
||||
this._onPressFingerprintAuth("Unlock note", "")
|
||||
this._onPressFingerprintAuth(strings.unlockNote(), "")
|
||||
}
|
||||
icon="fingerprint"
|
||||
width="100%"
|
||||
title={"Biometric unlock"}
|
||||
title={strings.unlockWithBiometrics()}
|
||||
type="transparent"
|
||||
/>
|
||||
)}
|
||||
@@ -788,7 +755,7 @@ export class VaultDialog extends Component {
|
||||
marginTop: 10
|
||||
}}
|
||||
width="100%"
|
||||
title={"Delete all notes"}
|
||||
title={strings.deleteAllNotes()}
|
||||
type="errorShade"
|
||||
/>
|
||||
)}
|
||||
@@ -809,7 +776,7 @@ export class VaultDialog extends Component {
|
||||
returnKeyLabel="Change"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
placeholder={"New password"}
|
||||
placeholder={strings.newPassword()}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
@@ -824,13 +791,13 @@ export class VaultDialog extends Component {
|
||||
this.password = value;
|
||||
}}
|
||||
autoComplete="password"
|
||||
returnKeyLabel="Next"
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
onSubmit={() => {
|
||||
this.confirmPassRef.current?.focus();
|
||||
}}
|
||||
placeholder="Password"
|
||||
placeholder={strings.password()}
|
||||
/>
|
||||
|
||||
<Input
|
||||
@@ -859,7 +826,7 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
}}
|
||||
onSubmit={this.onPress}
|
||||
placeholder="Confirm password"
|
||||
placeholder={strings.confirmPassword()}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
@@ -867,9 +834,7 @@ export class VaultDialog extends Component {
|
||||
{this.state.biometricUnlock &&
|
||||
!this.state.isBiometryEnrolled &&
|
||||
novault ? (
|
||||
<Paragraph>
|
||||
Unlock with password once to enable biometric access.
|
||||
</Paragraph>
|
||||
<Paragraph>{strings.vaultEnableBiometrics()}</Paragraph>
|
||||
) : null}
|
||||
|
||||
{this.state.isBiometryAvailable &&
|
||||
@@ -888,7 +853,7 @@ export class VaultDialog extends Component {
|
||||
}}
|
||||
icon="fingerprint"
|
||||
width="100%"
|
||||
title="Biometric unlocking"
|
||||
title={strings.unlockWithBiometrics()}
|
||||
iconColor={
|
||||
this.state.biometricUnlock
|
||||
? colors.selected.accent
|
||||
@@ -908,26 +873,26 @@ export class VaultDialog extends Component {
|
||||
}
|
||||
positiveTitle={
|
||||
deleteVault
|
||||
? "Delete"
|
||||
? strings.delete()
|
||||
: clearVault
|
||||
? "Clear"
|
||||
? strings.clear()
|
||||
: fingerprintAccess
|
||||
? "Enable"
|
||||
? strings.enable()
|
||||
: this.state.revokeFingerprintAccess
|
||||
? "Revoke"
|
||||
? strings.revoke()
|
||||
: changePassword
|
||||
? "Change"
|
||||
? strings.change()
|
||||
: this.state.noteLocked
|
||||
? deleteNote
|
||||
? "Delete"
|
||||
? strings.delete()
|
||||
: share
|
||||
? "Share "
|
||||
? strings.share()
|
||||
: goToEditor
|
||||
? "Open"
|
||||
: "Unlock"
|
||||
? strings.open()
|
||||
: strings.unlock()
|
||||
: !note.id
|
||||
? "Create"
|
||||
: "Lock"
|
||||
? strings.create()
|
||||
: strings.lock()
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -33,7 +33,6 @@ import { eScrollEvent } from "../../utils/events";
|
||||
import { LeftMenus } from "./left-menus";
|
||||
import { RightMenus } from "./right-menus";
|
||||
import { Title } from "./title";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
type HeaderRightButton = {
|
||||
title: string;
|
||||
|
||||
@@ -17,10 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { Linking, ScrollView, useWindowDimensions, View } from "react-native";
|
||||
import { SwiperFlatList } from "react-native-swiper-flatlist";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -33,37 +35,13 @@ import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
type IntroItem = {
|
||||
headings?: string[];
|
||||
body?: string;
|
||||
testimonial?: string;
|
||||
link?: string;
|
||||
user?: string;
|
||||
};
|
||||
|
||||
const introItems: IntroItem[] = [
|
||||
{
|
||||
headings: ["Open source.", "End to end encrypted.", "Private."],
|
||||
body: "Write notes with freedom, no spying, no tracking."
|
||||
},
|
||||
{
|
||||
headings: ["Privacy for everyone", "— not just the", "privileged few"],
|
||||
body: "Your privacy matters to us, no matter who you are. In a world where everyone is trying to spy on you, Notesnook encrypts all your data before it leaves your device. With Notesnook no one can ever sell your data again."
|
||||
},
|
||||
{
|
||||
testimonial:
|
||||
"You simply cannot get any better of a note taking app than @notesnook. The UI is clean and slick, it is feature rich, encrypted, reasonably priced (esp. for students & educators) & open source",
|
||||
link: "https://twitter.com/andrewsayer/status/1637817220113002503",
|
||||
user: "@andrewsayer on Twitter"
|
||||
}
|
||||
];
|
||||
|
||||
const Intro = () => {
|
||||
const Intro = ({ navigation }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { width } = useWindowDimensions();
|
||||
const { width, height } = useWindowDimensions();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const renderItem = React.useCallback(
|
||||
({ item }: { item: IntroItem }) => (
|
||||
({ item }) => (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
@@ -105,31 +83,32 @@ const Intro = () => {
|
||||
>
|
||||
{item.headings?.map((heading) => (
|
||||
<Heading
|
||||
key={heading}
|
||||
key={heading()}
|
||||
style={{
|
||||
marginBottom: 5
|
||||
}}
|
||||
extraBold
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
{heading}
|
||||
{heading()}
|
||||
</Heading>
|
||||
))}
|
||||
|
||||
{item.body ? <Paragraph size={SIZE.sm}>{item.body}</Paragraph> : null}
|
||||
{item.body ? (
|
||||
<Paragraph size={SIZE.sm}>{item.body()}</Paragraph>
|
||||
) : null}
|
||||
|
||||
{item.testimonial ? (
|
||||
{item.tesimonial ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
fontSize: SIZE.lg
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!item.link) return;
|
||||
Linking.openURL(item.link);
|
||||
}}
|
||||
>
|
||||
{item.testimonial} — {item.user}
|
||||
{item.tesimonial()} — {item.user}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -144,23 +123,22 @@ const Intro = () => {
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: deviceMode !== "mobile" ? width / 2 : "100%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : undefined,
|
||||
borderColor:
|
||||
deviceMode !== "mobile" ? colors.primary.border : undefined,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : undefined,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : undefined,
|
||||
flexGrow: 2
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
paddingTop: insets.top + 10,
|
||||
paddingBottom: insets.top + 10,
|
||||
minHeight: height * 0.7
|
||||
}}
|
||||
>
|
||||
<SwiperFlatList
|
||||
@@ -170,7 +148,7 @@ const Intro = () => {
|
||||
index={0}
|
||||
useReactNativeGestureHandler={true}
|
||||
showPagination
|
||||
data={introItems}
|
||||
data={strings.introData}
|
||||
paginationActiveColor={colors.primary.accent}
|
||||
paginationStyleItem={{
|
||||
width: 10,
|
||||
@@ -186,14 +164,13 @@ const Intro = () => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
gap: 12,
|
||||
paddingVertical: 16
|
||||
justifyContent: "center",
|
||||
minHeight: height * 0.3
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
width="100%"
|
||||
onPress={() => {
|
||||
width={250}
|
||||
onPress={async () => {
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
|
||||
setTimeout(() => {
|
||||
SettingsService.set({
|
||||
@@ -207,28 +184,12 @@ const Intro = () => {
|
||||
style={{
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "center",
|
||||
...getElevationStyle(5)
|
||||
...getElevationStyle(5),
|
||||
borderRadius: 100
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title="Get started"
|
||||
/>
|
||||
|
||||
<Button
|
||||
width="100%"
|
||||
title="I already have an account"
|
||||
type="secondary"
|
||||
onPress={() => {
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.login);
|
||||
setTimeout(() => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.replace("Notes", {
|
||||
canGoBack: false
|
||||
});
|
||||
}, 1000);
|
||||
}}
|
||||
title={strings.getStarted()}
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
125
apps/mobile/app/components/intro/welcome.js
Normal file
125
apps/mobile/app/components/intro/welcome.js
Normal file
File diff suppressed because one or more lines are too long
@@ -29,6 +29,7 @@ import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const NotebookHeader = ({
|
||||
notebook,
|
||||
@@ -57,7 +58,7 @@ export const NotebookHeader = ({
|
||||
}
|
||||
});
|
||||
ToastManager.show({
|
||||
heading: "Shortcut created",
|
||||
heading: strings.shortcutCreated(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
@@ -148,11 +149,7 @@ export const NotebookHeader = ({
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{notebook && totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
? totalNotes + " note"
|
||||
: "0 notes"}
|
||||
{strings.notes(totalNotes || 0)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { GroupHeader, GroupOptions, ItemType } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
@@ -26,7 +27,6 @@ import { presentSheet } from "../../../services/event-manager";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { RouteName } from "../../../stores/use-navigation-store";
|
||||
import { getContainerBorder } from "../../../utils/colors";
|
||||
import { GROUP } from "../../../utils/constants";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import Sort from "../../sheets/sort";
|
||||
import { Button } from "../../ui/button";
|
||||
@@ -57,17 +57,14 @@ export const SectionHeader = React.memo<
|
||||
}: SectionHeaderProps) {
|
||||
const { colors } = useThemeColors();
|
||||
const { fontScale } = useWindowDimensions();
|
||||
let groupBy = Object.keys(GROUP).find(
|
||||
(key) => GROUP[key as keyof typeof GROUP] === groupOptions.groupBy
|
||||
);
|
||||
const groupBy =
|
||||
strings.groupByStrings[
|
||||
groupOptions.groupBy as keyof typeof strings.groupByStrings
|
||||
]?.();
|
||||
const isCompactModeEnabled = useIsCompactModeEnabled(
|
||||
dataType as "note" | "notebook"
|
||||
);
|
||||
|
||||
groupBy = !groupBy
|
||||
? "Default"
|
||||
: groupBy.slice(0, 1).toUpperCase() + groupBy.slice(1, groupBy.length);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -103,7 +100,7 @@ export const SectionHeader = React.memo<
|
||||
textAlignVertical: "center"
|
||||
}}
|
||||
>
|
||||
{!item.title || item.title === "" ? "Pinned" : item.title}
|
||||
{!item.title || item.title === "" ? strings.pinned() : item.title}
|
||||
</Heading>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -117,12 +114,10 @@ export const SectionHeader = React.memo<
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
console.log("Opening Sort sheet", screen, dataType);
|
||||
presentSheet({
|
||||
component: <Sort screen={screen} type={dataType} />
|
||||
});
|
||||
}}
|
||||
tooltipText="Change sorting of items in list"
|
||||
title={groupBy}
|
||||
icon={
|
||||
groupOptions.sortDirection === "asc"
|
||||
@@ -156,11 +151,6 @@ export const SectionHeader = React.memo<
|
||||
screen !== "Notes"
|
||||
}
|
||||
testID="icon-compact-mode"
|
||||
tooltipText={
|
||||
isCompactModeEnabled
|
||||
? "Switch to normal mode"
|
||||
: "Switch to compact mode"
|
||||
}
|
||||
color={colors.secondary.icon}
|
||||
name={isCompactModeEnabled ? "view-list" : "view-list-outline"}
|
||||
onPress={() => {
|
||||
|
||||
@@ -46,6 +46,7 @@ import { ReminderTime } from "../../ui/reminder-time";
|
||||
import { TimeSince } from "../../ui/time-since";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type NoteItemProps = {
|
||||
item: Note | BaseTrashItem<Note>;
|
||||
@@ -341,9 +342,10 @@ const NoteItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
Deleted on{" "}
|
||||
{item && item.dateDeleted
|
||||
? new Date(item.dateDeleted).toISOString().slice(0, 10)
|
||||
? strings.deletedOn(
|
||||
new Date(item.dateDeleted).toISOString().slice(0, 10)
|
||||
)
|
||||
: null}
|
||||
</Paragraph>
|
||||
|
||||
@@ -354,8 +356,7 @@ const NoteItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{(item as TrashItem).itemType[0].toUpperCase() +
|
||||
(item as TrashItem).itemType.slice(1)}
|
||||
{strings.note()}
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { Properties } from "../../properties";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type NotebookItemProps = {
|
||||
item: Notebook | BaseTrashItem<Notebook>;
|
||||
@@ -112,10 +113,11 @@ export const NotebookItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{"Deleted on " +
|
||||
{strings.deletedOn(
|
||||
new Date((item as TrashItem).dateDeleted)
|
||||
.toISOString()
|
||||
.slice(0, 10)}
|
||||
.slice(0, 10)
|
||||
)}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
color={colors.primary.accent}
|
||||
@@ -147,11 +149,7 @@ export const NotebookItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{item && totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
? totalNotes + " note"
|
||||
: "0 notes"}
|
||||
{strings.notes(totalNotes)}
|
||||
</Paragraph>
|
||||
|
||||
{item.pinned ? (
|
||||
@@ -183,11 +181,7 @@ export const NotebookItem = ({
|
||||
marginRight: 6
|
||||
}}
|
||||
>
|
||||
{item && totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
? totalNotes + " note"
|
||||
: "0 notes"}
|
||||
{strings.notes(totalNotes)}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useSelectionStore } from "../../../stores/use-selection-store";
|
||||
import { useTrashStore } from "../../../stores/use-trash-store";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
const isTrash = item.type === "trash";
|
||||
@@ -36,16 +37,15 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
|
||||
if (isTrash) {
|
||||
presentDialog({
|
||||
title: `Restore notebook`,
|
||||
paragraph: `Restore or delete notebook forever`,
|
||||
positiveText: "Restore",
|
||||
negativeText: "Delete",
|
||||
title: strings.restoreNotebook(),
|
||||
positiveText: strings.restore(),
|
||||
negativeText: strings.delete(),
|
||||
positivePress: async () => {
|
||||
await db.trash.restore(item.id);
|
||||
if ((await db.trash.restore(item.id)) === false) return;
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(undefined);
|
||||
ToastManager.show({
|
||||
heading: "Restore successful",
|
||||
heading: strings.notebookRestored(),
|
||||
type: "success"
|
||||
});
|
||||
},
|
||||
@@ -54,7 +54,7 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
|
||||
useTrashStore.getState().refresh();
|
||||
useSelectionStore.getState().setSelectionMode(undefined);
|
||||
ToastManager.show({
|
||||
heading: "Permanently deleted items",
|
||||
heading: strings.permanentlyDeletedNotebook(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ import { ReminderTime } from "../../ui/reminder-time";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const ReminderItem = React.memo(
|
||||
({
|
||||
@@ -108,7 +109,7 @@ const ReminderItem = React.memo(
|
||||
color={colors.secondary.paragraph}
|
||||
style={{ marginLeft: 5 }}
|
||||
>
|
||||
Disabled
|
||||
{strings.disabled()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
@@ -137,9 +138,7 @@ const ReminderItem = React.memo(
|
||||
color={colors.secondary.paragraph}
|
||||
style={{ marginLeft: 5 }}
|
||||
>
|
||||
{item.recurringMode.slice(0, 1).toUpperCase() +
|
||||
item.recurringMode.replace("y", "i").slice(1) +
|
||||
"ly"}
|
||||
{strings.reminderRecurringMode[item.recurringMode]()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { IconButton } from "../../ui/icon-button";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import SelectionWrapper, { selectItem } from "../selection-wrapper";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const TagItem = React.memo(
|
||||
({
|
||||
@@ -77,11 +78,7 @@ const TagItem = React.memo(
|
||||
marginTop: 5
|
||||
}}
|
||||
>
|
||||
{totalNotes > 1
|
||||
? totalNotes + " notes"
|
||||
: totalNotes === 1
|
||||
? totalNotes + " note"
|
||||
: null}
|
||||
{strings.notes(totalNotes)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -87,8 +87,8 @@ export const Empty = React.memo(function Empty({
|
||||
color={color}
|
||||
tip={
|
||||
screen !== "Search"
|
||||
? tip || ({ text: placeholder?.paragraph } as TTip)
|
||||
: ({ text: placeholder?.paragraph } as TTip)
|
||||
? tip || ({ text: () => placeholder?.paragraph } as TTip)
|
||||
: ({ text: () => placeholder?.paragraph } as TTip)
|
||||
}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
|
||||
@@ -34,12 +34,13 @@ import {
|
||||
Reminder,
|
||||
Tag,
|
||||
TrashItem,
|
||||
VirtualizedGrouping
|
||||
VirtualizedGrouping,
|
||||
getSortValue
|
||||
} from "@notesnook/core";
|
||||
import { getSortValue } from "@notesnook/core";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { RouteName } from "../../stores/use-navigation-store";
|
||||
import { eOpenJumpToDialog } from "../../utils/events";
|
||||
@@ -48,7 +49,6 @@ import { NoteWrapper } from "../list-items/note/wrapper";
|
||||
import { NotebookWrapper } from "../list-items/notebook/wrapper";
|
||||
import ReminderItem from "../list-items/reminder";
|
||||
import TagItem from "../list-items/tag";
|
||||
import { useIsCompactModeEnabled } from "../../hooks/use-is-compact-mode-enabled";
|
||||
|
||||
type ListItemWrapperProps<TItem = Item> = {
|
||||
group?: GroupingKey;
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
export const Loading = (props: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16
|
||||
}}
|
||||
>
|
||||
{props.icon ? (
|
||||
<Icon name={props.icon} size={80} color={colors.primary.accent} />
|
||||
) : null}
|
||||
|
||||
{props.title ? (
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{props.description ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{props.description}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
marginTop: 15
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -48,6 +48,7 @@ import { IconButton } from "../ui/icon-button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { diff } from "diffblazer";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const MergeConflicts = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -171,7 +172,11 @@ const MergeConflicts = () => {
|
||||
fontWeight: "bold"
|
||||
}}
|
||||
>
|
||||
{isCurrent ? "(This Device)" : "(Incoming)"}
|
||||
(
|
||||
{isCurrent
|
||||
? strings.mergeConflict.thisDevice()
|
||||
: strings.mergeConflict.otherDevice()}
|
||||
)
|
||||
</Text>
|
||||
{"\n"}
|
||||
{getFormattedDate(contentToKeep?.dateEdited)}
|
||||
@@ -191,7 +196,7 @@ const MergeConflicts = () => {
|
||||
setCopy(contentToKeep);
|
||||
setDialogVisible(true);
|
||||
}}
|
||||
title="Save a copy"
|
||||
title={strings.saveACopy()}
|
||||
type="secondary"
|
||||
height={30}
|
||||
style={{
|
||||
@@ -204,7 +209,7 @@ const MergeConflicts = () => {
|
||||
<View style={{ width: 10 }} />
|
||||
{isDiscarded ? (
|
||||
<Button
|
||||
title="Discard"
|
||||
title={strings.discard()}
|
||||
type="accent"
|
||||
buttonType={{
|
||||
color: colors.static.red,
|
||||
@@ -234,7 +239,9 @@ const MergeConflicts = () => {
|
||||
}}
|
||||
type="accent"
|
||||
fontSize={SIZE.xs}
|
||||
title={keeping && !isDiscarded ? "Undo" : "Keep"}
|
||||
title={
|
||||
keeping && !isDiscarded ? strings.undo() : strings.keep()
|
||||
}
|
||||
onPress={() => {
|
||||
setKeep(keeping && !isDiscarded ? null : contentToKeep);
|
||||
}}
|
||||
@@ -277,11 +284,7 @@ const MergeConflicts = () => {
|
||||
{dialogVisible && (
|
||||
<BaseDialog visible={true}>
|
||||
<DialogContainer>
|
||||
<DialogHeader
|
||||
title="Apply Changes"
|
||||
paragraph="Apply selected changes to note?"
|
||||
padding={12}
|
||||
/>
|
||||
<DialogHeader title={strings.applyChanges()} padding={12} />
|
||||
<Seperator />
|
||||
<DialogButtons
|
||||
positiveTitle="Apply"
|
||||
|
||||
@@ -37,6 +37,7 @@ import { Pressable } from "../ui/pressable";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import NotePreview from "./preview";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const HistoryItem = ({
|
||||
index,
|
||||
@@ -141,11 +142,7 @@ export default function NoteHistory({
|
||||
return (
|
||||
<View>
|
||||
<SheetProvider context="note_history" />
|
||||
<DialogHeader
|
||||
title="Note history"
|
||||
paragraph="Revert back to an older version of this note"
|
||||
padding={12}
|
||||
/>
|
||||
<DialogHeader title={strings.noteHistory()} padding={12} />
|
||||
|
||||
<Seperator />
|
||||
|
||||
@@ -180,7 +177,7 @@ export default function NoteHistory({
|
||||
<>
|
||||
<Icon name="history" size={50} color={colors.primary.icon} />
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
No note history found on this device.
|
||||
{strings.noteHistoryPlaceholder()}
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
@@ -196,7 +193,7 @@ export default function NoteHistory({
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
Note version history is local only.{" "}
|
||||
{strings.noteHistoryNotice[0]()}{" "}
|
||||
<Text
|
||||
onPress={() => {
|
||||
openLinkInBrowser(
|
||||
@@ -208,7 +205,7 @@ export default function NoteHistory({
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
>
|
||||
Learn how this works.
|
||||
{strings.noteHistoryNotice[1]()}
|
||||
</Text>
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
@@ -35,6 +35,7 @@ import { presentDialog } from "../dialog/functions";
|
||||
import { Button } from "../ui/button";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { diff } from "diffblazer";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -47,11 +48,11 @@ export default function NotePreview({ session, content, note }) {
|
||||
|
||||
async function restore() {
|
||||
if (note && note.type === "trash") {
|
||||
await db.trash.restore(note.id);
|
||||
if ((await db.trash.restore(note.id)) === false) return;
|
||||
Navigation.queueRoutesForUpdate();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastManager.show({
|
||||
heading: "Restore successful",
|
||||
heading: strings.noteRestored(),
|
||||
type: "success"
|
||||
});
|
||||
eSendEvent(eCloseSheet);
|
||||
@@ -72,7 +73,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
ToastManager.show({
|
||||
heading: "Note restored successfully",
|
||||
heading: strings.noteRestoredFromHistory(),
|
||||
type: "success"
|
||||
});
|
||||
}
|
||||
@@ -83,17 +84,17 @@ export default function NotePreview({ session, content, note }) {
|
||||
|
||||
const deleteNote = async () => {
|
||||
presentDialog({
|
||||
title: `Delete note permanently`,
|
||||
paragraph: `Are you sure you want to delete this note from trash permanentaly`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
title: strings.deleteNote(),
|
||||
paragraph: strings.deleteNoteConfirmation(),
|
||||
positiveText: strings.delete(),
|
||||
negativeText: strings.cancel(),
|
||||
context: "local",
|
||||
positivePress: async () => {
|
||||
await db.trash.delete(note.id);
|
||||
useTrashStore.getState().refresh();
|
||||
useSelectionStore.getState().setSelectionMode(false);
|
||||
ToastManager.show({
|
||||
heading: "Permanently deleted items",
|
||||
heading: strings.noteDeleted(),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
@@ -150,7 +151,7 @@ export default function NotePreview({ session, content, note }) {
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
Preview not available, content is encrypted.
|
||||
{strings.encryptedNoteHistoryNotice()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
)}
|
||||
@@ -160,10 +161,15 @@ export default function NotePreview({ session, content, note }) {
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Button onPress={restore} title="Restore" type="accent" width="100%" />
|
||||
<Button
|
||||
onPress={restore}
|
||||
title={strings.restore()}
|
||||
type="accent"
|
||||
width="100%"
|
||||
/>
|
||||
<Button
|
||||
onPress={deleteNote}
|
||||
title="Delete permanently"
|
||||
title={strings.deletePermanently()}
|
||||
type="error"
|
||||
width="100%"
|
||||
style={{
|
||||
|
||||
292
apps/mobile/app/components/premium/component.js
Normal file
292
apps/mobile/app/components/premium/component.js
Normal file
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
|
||||
import { LAUNCH_ROCKET } from "../../assets/images/assets";
|
||||
import { db } from "../../common/database";
|
||||
import { usePricing } from "../../hooks/use-pricing";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import {
|
||||
eClosePremiumDialog,
|
||||
eCloseSheet,
|
||||
eOpenLoginDialog
|
||||
} from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { AuthMode } from "../auth";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import { features } from "./features";
|
||||
import { Group } from "./group";
|
||||
import { PricingPlans } from "./pricing-plans";
|
||||
|
||||
export const Component = ({ close, promo }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const userCanRequestTrial =
|
||||
user && (!user.subscription || !user.subscription.expiry) ? true : false;
|
||||
const [floatingButton, setFloatingButton] = useState(false);
|
||||
const pricing = usePricing("monthly");
|
||||
|
||||
const onPress = async () => {
|
||||
if (user) {
|
||||
presentSheet({
|
||||
context: "pricing_plans",
|
||||
component: (
|
||||
<PricingPlans showTrialOption={false} marginTop={1} promo={promo} />
|
||||
)
|
||||
});
|
||||
} else {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.trialSignup);
|
||||
}, 400);
|
||||
}
|
||||
};
|
||||
|
||||
const onScroll = (event) => {
|
||||
let contentSize = event.nativeEvent.contentSize.height;
|
||||
contentSize = contentSize - event.nativeEvent.layoutMeasurement.height;
|
||||
let yOffset = event.nativeEvent.contentOffset.y;
|
||||
if (yOffset > 600 && yOffset < contentSize - 400) {
|
||||
setFloatingButton(true);
|
||||
} else {
|
||||
setFloatingButton(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "space-between",
|
||||
borderRadius: 10,
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<SheetProvider context="pricing_plans" />
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
close();
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: DDS.isTab ? 30 : 15,
|
||||
top: Platform.OS === "ios" ? 0 : 30,
|
||||
zIndex: 10,
|
||||
width: 50,
|
||||
height: 50
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
name="close"
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
style={{
|
||||
paddingHorizontal: DDS.isTab ? DDS.width / 5 : 0
|
||||
}}
|
||||
scrollEventThrottle={0}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<View
|
||||
key="top-banner"
|
||||
style={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
height: 400,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<SvgView
|
||||
width={350}
|
||||
height={350}
|
||||
src={LAUNCH_ROCKET(colors.primary.accent)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Heading
|
||||
key="heading"
|
||||
size={SIZE.lg}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
paddingTop: 20
|
||||
}}
|
||||
>
|
||||
Notesnook{" "}
|
||||
<Heading size={SIZE.lg} color={colors.primary.accent}>
|
||||
Pro
|
||||
</Heading>
|
||||
</Heading>
|
||||
|
||||
{!pricing ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={SIZE.md}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
) : (
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
(
|
||||
{Platform.OS === "android"
|
||||
? pricing.product?.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].formattedPrice
|
||||
: pricing.product?.localizedPrice}{" "}
|
||||
/ mo)
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
key="description"
|
||||
size={SIZE.md}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
textAlign: "center",
|
||||
alignSelf: "center",
|
||||
paddingBottom: 20,
|
||||
width: "90%"
|
||||
}}
|
||||
>
|
||||
Ready to take the next step on your private note taking journey?
|
||||
</Paragraph>
|
||||
|
||||
{userCanRequestTrial ? (
|
||||
<Button
|
||||
key="calltoaction"
|
||||
onPress={async () => {
|
||||
try {
|
||||
await db.user.activateTrial();
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Walkthrough.present("trialstarted", false, true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
title="Try free for 14 days"
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 15,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
key="calltoaction"
|
||||
onPress={onPress}
|
||||
title={
|
||||
promo ? promo.text : user ? "See all plans" : "Sign up for free"
|
||||
}
|
||||
type={userCanRequestTrial ? "secondaryAccented" : "accent"}
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 15,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
{!user || userCanRequestTrial ? (
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center",
|
||||
marginTop: 10,
|
||||
maxWidth: "80%"
|
||||
}}
|
||||
>
|
||||
{user
|
||||
? 'On clicking "Try free for 14 days", your free trial will be activated.'
|
||||
: "After sign up you will be asked to activate your free trial."}{" "}
|
||||
<Paragraph size={SIZE.xs} style={{ fontWeight: "bold" }}>
|
||||
No credit card is required.
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Seperator key="seperator_1" />
|
||||
|
||||
{features.map((item, index) => (
|
||||
<Group key={item.title} item={item} index={index} />
|
||||
))}
|
||||
|
||||
<View
|
||||
key="plans"
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<PricingPlans showTrialOption={false} promo={promo} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{floatingButton ? (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={
|
||||
promo ? promo.text : user ? "See all plans" : "Sign up for free"
|
||||
}
|
||||
type="accent"
|
||||
style={{
|
||||
paddingHorizontal: 24,
|
||||
position: "absolute",
|
||||
borderRadius: 100,
|
||||
bottom: 30,
|
||||
...getElevationStyle(10)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export const FeaturesList = {
|
||||
// Monthly: ["$0/mo", "$1.99/mo", "$5.99/mo", "$9.99/mo"],
|
||||
// Yearly: ["$0/yr", "$14.99/yr", "$59.99/yr", "$109.99/yr"],
|
||||
// "5 year": [null, null, "$249.99/5yr", "$449.99/5yr"],
|
||||
Plans: ["Free", "Essential", "Pro", "Believer"],
|
||||
Devices: [2, 5, "Unlimited", "Unlimited"],
|
||||
Storage: ["100MB/mo", "1GB/mo", "10GB/mo", "25GB/mo"],
|
||||
"File size": ["10MB", "100MB", "1GB", "5GB"],
|
||||
"File compression": [true, true, "Optional", "Optional"],
|
||||
"Check list": [true, true, true, true],
|
||||
"Bullet list": [true, true, true, true],
|
||||
Quote: [true, true, true, true],
|
||||
"Note link": [true, true, true, true],
|
||||
"Task list": [false, true, true, true],
|
||||
"Outline list": [false, true, true, true],
|
||||
Callouts: [false, true, true, true],
|
||||
Colors: [7, 20, "∞", "∞"],
|
||||
Tags: [50, 500, "∞", "∞"],
|
||||
Notebooks: [100, 1000, "∞", "∞"],
|
||||
"Active Reminders": [10, 50, "∞", "∞"],
|
||||
Shortcuts: [10, "∞", "∞", "∞"],
|
||||
"Note history": ["Local", "Local", "Synced", "Synced"],
|
||||
"Security key app lock": [false, false, true, true],
|
||||
"Pin note in notification": [false, false, true, true],
|
||||
"Create note from notification drawer": [false, false, true, true],
|
||||
"Default startup screen": [false, false, true, true],
|
||||
"Markdown shortcuts": [false, true, true, true],
|
||||
"Custom toolbar preset": [false, false, true, true],
|
||||
"Customizable navigation menu": [false, true, true, true],
|
||||
"Files in monograph": [
|
||||
"Images",
|
||||
"Images & files",
|
||||
"Images & files",
|
||||
"Images & files"
|
||||
],
|
||||
"Note history retention": ["1 month", "∞", "∞", "∞"],
|
||||
"App lock": ["immediate", "immediate", "any", "any"],
|
||||
"Password on monograph": ["Default", "Custom", "Custom", "Custom"],
|
||||
"Voice memos": ["5 minute", "1 hour", "∞", true],
|
||||
"Notebook publishing": [1, 2, 5, true],
|
||||
Vaults: [1, 3, "∞", "∞"]
|
||||
};
|
||||
@@ -24,8 +24,7 @@ import {
|
||||
} from "../../services/event-manager";
|
||||
import { eClosePremiumDialog, eOpenPremiumDialog } from "../../utils/events";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { PaywallComponent } from "./component";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { Component } from "./component";
|
||||
|
||||
class PremiumDialog extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -75,7 +74,7 @@ class PremiumDialog extends React.Component {
|
||||
background={this.props.colors.primary.background}
|
||||
onRequestClose={this.onClose}
|
||||
>
|
||||
<PaywallComponent
|
||||
<Component
|
||||
getRef={() => this.actionSheetRef}
|
||||
promo={this.state.promo}
|
||||
close={this.close}
|
||||
|
||||
@@ -48,7 +48,7 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import { PricingItem } from "./pricing-item";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import usePricingPlans from "../../hooks/use-pricing-plans";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const UUID_PREFIX = "0bdaea";
|
||||
const UUID_VERSION = "4";
|
||||
@@ -93,21 +93,173 @@ export const PricingPlans = ({
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const {
|
||||
buySubscription,
|
||||
buying,
|
||||
getStandardPrice,
|
||||
product,
|
||||
setBuying,
|
||||
loading,
|
||||
user,
|
||||
setProduct,
|
||||
monthlyPlan,
|
||||
yearlyPlan,
|
||||
getPromo
|
||||
} = usePricingPlans({
|
||||
promoOffer: promo
|
||||
});
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [product, setProduct] = useState<{
|
||||
type: string;
|
||||
offerType: "monthly" | "yearly";
|
||||
data: RNIap.Subscription;
|
||||
cycleText: string;
|
||||
info: string;
|
||||
}>();
|
||||
|
||||
const [buying, setBuying] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const userCanRequestTrial =
|
||||
user && (!user.subscription || !user.subscription.expiry) ? true : false;
|
||||
const [upgrade, setUpgrade] = useState(!userCanRequestTrial);
|
||||
const yearlyPlan = usePricing("yearly");
|
||||
const monthlyPlan = usePricing("monthly");
|
||||
|
||||
const getSkus = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
if (promo?.promoCode) {
|
||||
getPromo(promo?.promoCode);
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
console.log("error getting sku", e);
|
||||
}
|
||||
}, [promo?.promoCode]);
|
||||
|
||||
const getPromo = async (code: string) => {
|
||||
try {
|
||||
let skuId: string;
|
||||
if (code.startsWith("com.streetwriters.notesnook")) {
|
||||
skuId = code;
|
||||
} else {
|
||||
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
|
||||
}
|
||||
|
||||
const products = await PremiumService.getProducts();
|
||||
const product = products.find((p) => p.productId === skuId);
|
||||
if (!product) return false;
|
||||
const isMonthly = product.productId.indexOf(".mo") > -1;
|
||||
|
||||
const cycleText = isMonthly
|
||||
? promoCyclesMonthly[
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid)
|
||||
.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesMonthly
|
||||
]
|
||||
: promoCyclesYearly[
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid)
|
||||
.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesYearly
|
||||
];
|
||||
|
||||
setProduct({
|
||||
type: "promo",
|
||||
offerType: isMonthly ? "monthly" : "yearly",
|
||||
data: product,
|
||||
cycleText: cycleText,
|
||||
info: `Pay ${isMonthly ? "monthly" : "yearly"}, cancel anytime`
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log("PROMOCODE ERROR:", code, e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSkus();
|
||||
}, [getSkus]);
|
||||
|
||||
const buySubscription = async (product: RNIap.Subscription) => {
|
||||
if (buying || !product) return;
|
||||
setBuying(true);
|
||||
try {
|
||||
if (!user) {
|
||||
setBuying(false);
|
||||
return;
|
||||
}
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
const androidOfferToken =
|
||||
Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
|
||||
.offerToken
|
||||
: null;
|
||||
|
||||
DatabaseLogger.info(
|
||||
`Subscription Requested initiated for user ${toUUID(user.id)}`
|
||||
);
|
||||
|
||||
await RNIap.requestSubscription({
|
||||
sku: product?.productId,
|
||||
obfuscatedAccountIdAndroid: user.id,
|
||||
obfuscatedProfileIdAndroid: user.id,
|
||||
appAccountToken: toUUID(user.id),
|
||||
andDangerouslyFinishTransactionAutomaticallyIOS: false,
|
||||
subscriptionOffers: androidOfferToken
|
||||
? [
|
||||
{
|
||||
offerToken: androidOfferToken,
|
||||
sku: product?.productId
|
||||
}
|
||||
]
|
||||
: undefined
|
||||
});
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
setBuying(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
await sleep(500);
|
||||
presentSheet({
|
||||
title: "Thank you for subscribing!",
|
||||
paragraph:
|
||||
"Your Notesnook Pro subscription will be activated soon. If your account is not upgraded to Notesnook Pro, your money will be refunded to you. In case of any issues, please reach out to us at support@streetwriters.co",
|
||||
action: async () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
},
|
||||
icon: "check",
|
||||
actionText: "Continue"
|
||||
});
|
||||
} catch (e) {
|
||||
setBuying(false);
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
function getStandardPrice() {
|
||||
if (!product) return;
|
||||
const productType = product.offerType;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const pricingPhaseListItem = (product.data as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[1];
|
||||
|
||||
if (!pricingPhaseListItem) {
|
||||
const product =
|
||||
productType === "monthly"
|
||||
? monthlyPlan?.product
|
||||
: yearlyPlan?.product;
|
||||
return (product as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[0]
|
||||
?.formattedPrice;
|
||||
}
|
||||
|
||||
return pricingPhaseListItem?.formattedPrice;
|
||||
} else {
|
||||
const productDefault =
|
||||
productType === "monthly" ? monthlyPlan?.product : yearlyPlan?.product;
|
||||
return (
|
||||
(product.data as RNIap.SubscriptionIOS)?.localizedPrice ||
|
||||
(productDefault as RNIap.SubscriptionIOS)?.localizedPrice
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return loading ? (
|
||||
<View
|
||||
@@ -127,215 +279,354 @@ export const PricingPlans = ({
|
||||
}}
|
||||
>
|
||||
{buying ? (
|
||||
<BaseDialog visible statusBarTranslucent centered>
|
||||
<BaseDialog statusBarTranslucent centered>
|
||||
<ActivityIndicator size={50} color="white" />
|
||||
</BaseDialog>
|
||||
) : null}
|
||||
|
||||
{!user && !product ? (
|
||||
{!upgrade ? (
|
||||
<>
|
||||
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<>
|
||||
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: marginTop || 20,
|
||||
marginBottom: 20
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center"
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
{(Platform.OS === "android"
|
||||
? (monthlyPlan?.product as RNIap.SubscriptionAndroid | undefined)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (monthlyPlan?.product as RNIap.SubscriptionIOS | undefined)
|
||||
?.localizedPrice) ||
|
||||
(PremiumService.getMontlySub() as any)?.localizedPrice}
|
||||
/ mo
|
||||
</Paragraph>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setUpgrade(true);
|
||||
}}
|
||||
title={"Upgrade now"}
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 15,
|
||||
marginTop: 15,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onPress={async () => {
|
||||
try {
|
||||
await db.user?.activateTrial();
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Walkthrough.present("trialstarted", false, true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
title={"Try free for 14 days"}
|
||||
type="secondaryAccented"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 15
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{product?.type === "promo" ? (
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 15,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{product?.offerType === "monthly" ? (
|
||||
<PricingItem
|
||||
product={{
|
||||
type: "monthly",
|
||||
data: monthlyPlan?.product,
|
||||
info: "Pay once a month, cancel anytime."
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Get {monthlyPlan?.info?.discount}% off in{" "}
|
||||
{monthlyPlan?.info?.country}
|
||||
</Heading>
|
||||
</View>
|
||||
strikethrough={true}
|
||||
/>
|
||||
) : (
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!monthlyPlan?.product) return;
|
||||
buySubscription(monthlyPlan?.product);
|
||||
}}
|
||||
product={{
|
||||
type: "yearly",
|
||||
data: yearlyPlan?.product,
|
||||
info: "Pay once a year, cancel anytime."
|
||||
}}
|
||||
strikethrough={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Heading
|
||||
style={{
|
||||
paddingTop: 15,
|
||||
fontSize: SIZE.lg
|
||||
}}
|
||||
>
|
||||
Special offer for you
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 20,
|
||||
paddingBottom: 10
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: marginTop || 20,
|
||||
marginBottom: 20
|
||||
}}
|
||||
>
|
||||
Choose a plan
|
||||
</Heading>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: !compact ? "column" : "row",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-around"
|
||||
}}
|
||||
>
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!monthlyPlan?.product) return;
|
||||
buySubscription(monthlyPlan?.product);
|
||||
}}
|
||||
compact={compact}
|
||||
product={{
|
||||
type: "monthly",
|
||||
data: monthlyPlan?.product,
|
||||
info: "Pay once a month, cancel anytime."
|
||||
}}
|
||||
/>
|
||||
|
||||
{!compact && (
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
marginVertical: 5
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!yearlyPlan?.product) return;
|
||||
buySubscription(yearlyPlan?.product);
|
||||
}}
|
||||
compact={compact}
|
||||
product={{
|
||||
type: "yearly",
|
||||
data: yearlyPlan?.product,
|
||||
info: "Pay once a year, cancel anytime."
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{Platform.OS !== "ios" ? (
|
||||
<Button
|
||||
height={35}
|
||||
style={{
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
context: "local",
|
||||
input: true,
|
||||
inputPlaceholder: "Enter code",
|
||||
positiveText: "Apply",
|
||||
positivePress: async (value) => {
|
||||
if (!value) return;
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
setBuying(true);
|
||||
try {
|
||||
if (!(await getPromo(value as string)))
|
||||
throw new Error("Error applying promo code");
|
||||
ToastManager.show({
|
||||
heading: "Discount applied!",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
setBuying(false);
|
||||
} catch (e) {
|
||||
setBuying(false);
|
||||
ToastManager.show({
|
||||
heading: "Promo code invalid or expired",
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
},
|
||||
title: "Have a promo code?",
|
||||
paragraph: "Enter your promo code to get a special discount."
|
||||
});
|
||||
}}
|
||||
title="I have a promo code"
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
height: 15
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<View>
|
||||
{!user ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
setTimeout(() => {
|
||||
eSendEvent(eOpenLoginDialog, 1);
|
||||
}, 400);
|
||||
}}
|
||||
title={"Sign up for free"}
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: product?.type === "promo" ? 0 : 30,
|
||||
marginBottom: 10
|
||||
}}
|
||||
/>
|
||||
{Platform.OS !== "ios" &&
|
||||
promo &&
|
||||
!promo.promoCode.startsWith("com.streetwriters.notesnook") ? (
|
||||
<Paragraph
|
||||
size={SIZE.md}
|
||||
textBreakStrategy="balanced"
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
justifyContent: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Use promo code{" "}
|
||||
<Text
|
||||
{Platform.OS === "android"
|
||||
? (product.data as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (product.data as RNIap.SubscriptionIOS)
|
||||
?.introductoryPrice ||
|
||||
(product.data as RNIap.SubscriptionIOS)
|
||||
?.localizedPrice}{" "}
|
||||
{product?.cycleText
|
||||
? `for ${product.cycleText}`
|
||||
: product?.offerType}
|
||||
</Heading>
|
||||
{product?.cycleText ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
fontFamily: "OpenSans-SemiBold"
|
||||
color: colors.secondary.paragraph,
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{promo.promoCode}
|
||||
</Text>{" "}
|
||||
at checkout
|
||||
</Paragraph>
|
||||
then {getStandardPrice()} {product?.offerType}.
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{user && !product ? (
|
||||
<>
|
||||
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<>
|
||||
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: marginTop || 20,
|
||||
marginBottom: 20
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Get {monthlyPlan?.info?.discount}% off in{" "}
|
||||
{monthlyPlan?.info?.country}
|
||||
</Heading>
|
||||
</View>
|
||||
) : (
|
||||
<Heading
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: marginTop || 20,
|
||||
marginBottom: 20
|
||||
}}
|
||||
>
|
||||
Choose a plan
|
||||
</Heading>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: !compact ? "column" : "row",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-around"
|
||||
}}
|
||||
>
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!monthlyPlan?.product) return;
|
||||
buySubscription(monthlyPlan?.product);
|
||||
}}
|
||||
compact={compact}
|
||||
product={{
|
||||
type: "monthly",
|
||||
data: monthlyPlan?.product,
|
||||
info: "Pay once a month, cancel anytime."
|
||||
}}
|
||||
/>
|
||||
|
||||
{!compact && (
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
marginVertical: 5
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!yearlyPlan?.product) return;
|
||||
buySubscription(yearlyPlan?.product);
|
||||
}}
|
||||
compact={compact}
|
||||
product={{
|
||||
type: "yearly",
|
||||
data: yearlyPlan?.product,
|
||||
info: "Pay once a year, cancel anytime."
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{Platform.OS !== "ios" ? (
|
||||
<Button
|
||||
height={35}
|
||||
style={{
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
context: "local",
|
||||
input: true,
|
||||
inputPlaceholder: "Enter code",
|
||||
positiveText: "Apply",
|
||||
positivePress: async (value) => {
|
||||
if (!value) return;
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
setBuying(true);
|
||||
try {
|
||||
if (!(await getPromo(value as string)))
|
||||
throw new Error(strings.errorApplyingPromoCode());
|
||||
ToastManager.show({
|
||||
heading: "Discount applied!",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
setBuying(false);
|
||||
} catch (e) {
|
||||
setBuying(false);
|
||||
ToastManager.show({
|
||||
heading: "Promo code invalid or expired",
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
},
|
||||
title: "Have a promo code?",
|
||||
paragraph:
|
||||
"Enter your promo code to get a special discount."
|
||||
});
|
||||
}}
|
||||
title="I have a promo code"
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
height: 15
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (!product?.data) return;
|
||||
buySubscription(product.data);
|
||||
}}
|
||||
height={40}
|
||||
width="50%"
|
||||
type="accent"
|
||||
title="Subscribe now"
|
||||
/>
|
||||
<View>
|
||||
{!user ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
setTimeout(() => {
|
||||
eSendEvent(eOpenLoginDialog, 1);
|
||||
}, 400);
|
||||
}}
|
||||
title={"Sign up for free"}
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: product?.type === "promo" ? 0 : 30,
|
||||
marginBottom: 10
|
||||
}}
|
||||
/>
|
||||
{Platform.OS !== "ios" &&
|
||||
promo &&
|
||||
!promo.promoCode.startsWith("com.streetwriters.notesnook") ? (
|
||||
<Paragraph
|
||||
size={SIZE.md}
|
||||
textBreakStrategy="balanced"
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
justifyContent: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Use promo code{" "}
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: "OpenSans-SemiBold"
|
||||
}}
|
||||
>
|
||||
{promo.promoCode}
|
||||
</Text>{" "}
|
||||
at checkout
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (!product?.data) return;
|
||||
buySubscription(product.data);
|
||||
}}
|
||||
height={40}
|
||||
width="50%"
|
||||
type="accent"
|
||||
title="Subscribe now"
|
||||
/>
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
setProduct(undefined);
|
||||
}}
|
||||
style={{
|
||||
marginTop: 5
|
||||
}}
|
||||
height={30}
|
||||
fontSize={13}
|
||||
type="errorShade"
|
||||
title="Cancel promo code"
|
||||
/>
|
||||
</>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setProduct(undefined);
|
||||
}}
|
||||
style={{
|
||||
marginTop: 5
|
||||
}}
|
||||
height={30}
|
||||
fontSize={13}
|
||||
type="errorShade"
|
||||
title="Cancel promo code"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!user ? (
|
||||
{!user || !upgrade ? (
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
@@ -355,7 +646,7 @@ export const PricingPlans = ({
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{user ? (
|
||||
{user && upgrade ? (
|
||||
<>
|
||||
{Platform.OS === "ios" ? (
|
||||
<Paragraph
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ProTag = ({ width, size, background }) => {
|
||||
marginRight: 3
|
||||
}}
|
||||
size={size}
|
||||
color={colors.static.orange}
|
||||
color={colors.primary.accent}
|
||||
name="crown"
|
||||
/>
|
||||
<Paragraph size={size - 1.5} color={colors.primary.accent}>
|
||||
|
||||
@@ -36,6 +36,7 @@ import ColorPicker from "../dialogs/color-picker";
|
||||
import { Button } from "../ui/button";
|
||||
import NativeTooltip from "../../utils/tooltip";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -145,7 +146,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
|
||||
buttonType={{
|
||||
text: colors.primary.accent
|
||||
}}
|
||||
title="Add color"
|
||||
title={strings.addColor()}
|
||||
type="secondary"
|
||||
icon="plus"
|
||||
iconPosition="right"
|
||||
|
||||
@@ -23,26 +23,10 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { strings } from "@notesnook/intl";
|
||||
export const DateMeta = ({ item }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const getNameFromKey = (key) => {
|
||||
switch (key) {
|
||||
case "dateCreated":
|
||||
return "Created at:";
|
||||
case "dateEdited":
|
||||
return "Last edited at:";
|
||||
case "dateModified":
|
||||
return "Last modified at:";
|
||||
case "dateDeleted":
|
||||
return "Deleted at:";
|
||||
case "dateUploaded":
|
||||
return "Uploaded at:";
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
};
|
||||
|
||||
function getDateMeta() {
|
||||
let keys = Object.keys(item);
|
||||
if (keys.includes("dateEdited"))
|
||||
@@ -64,7 +48,7 @@ export const DateMeta = ({ item }) => {
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getNameFromKey(key)}
|
||||
{strings.dateDescFromKey(key)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs} color={colors.secondary.paragraph}>
|
||||
{getFormattedDate(item[key], "date-time")}
|
||||
|
||||
@@ -39,6 +39,7 @@ import Notebooks from "./notebooks";
|
||||
import { Synced } from "./synced";
|
||||
import { TagStrip, Tags } from "./tags";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const Line = ({ top = 6, bottom = 6 }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -61,7 +62,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
|
||||
if (!item || !item.id) {
|
||||
return (
|
||||
<Paragraph style={{ marginVertical: 10, alignSelf: "center" }}>
|
||||
Start writing to save your note.
|
||||
{strings.noNotePropertiesNotice()}
|
||||
</Paragraph>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,14 +137,13 @@ export const Items = ({ item, buttons, close }) => {
|
||||
style={{
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: 0,
|
||||
width: topBarItemWidth
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
onPress={item.func}
|
||||
style={{
|
||||
height: topBarItemWidth,
|
||||
width: topBarItemWidth,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
textAlign: "center",
|
||||
@@ -169,7 +168,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
|
||||
<Paragraph
|
||||
textBreakStrategy="simple"
|
||||
size={SIZE.xxs + 1}
|
||||
size={SIZE.xxs}
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
{item.title}
|
||||
@@ -242,7 +241,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
paddingRight: 25,
|
||||
gap: 5
|
||||
gap: 15
|
||||
}}
|
||||
>
|
||||
{topBarItems.map(renderTopBarItem)}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export default function Notebooks({ note, close, full }) {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -102,7 +103,7 @@ export default function Notebooks({ note, close, full }) {
|
||||
|
||||
{noteNotebooks.length > 1 && !full ? (
|
||||
<Button
|
||||
title={`View all linked notebooks`}
|
||||
title={strings.viewAllLinkedNotebooks()}
|
||||
fontSize={SIZE.xs}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
|
||||
@@ -28,6 +28,7 @@ import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
export const Synced = ({ item, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
@@ -71,7 +72,7 @@ export const Synced = ({ item, close }) => {
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
>
|
||||
Encrypted and synced
|
||||
{strings.noteSyncedNoticeHeading()}
|
||||
</Heading>
|
||||
{shouldShrink ? null : (
|
||||
<Paragraph
|
||||
@@ -81,7 +82,7 @@ export const Synced = ({ item, close }) => {
|
||||
size={SIZE.xs}
|
||||
color={colors.primary.paragraph}
|
||||
>
|
||||
No one can view this {item.itemType || item.type} except you.
|
||||
{strings.noteSyncedNoticeDesc(item.itemType || item.type)}
|
||||
</Paragraph>
|
||||
)}
|
||||
</View>
|
||||
@@ -99,7 +100,7 @@ export const Synced = ({ item, close }) => {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
title="Learn more"
|
||||
title={strings.learnMore()}
|
||||
fontSize={SIZE.xs}
|
||||
height={30}
|
||||
type="secondaryAccented"
|
||||
|
||||
@@ -27,6 +27,7 @@ import { sleep } from "../../utils/time";
|
||||
import ManageTagsSheet from "../sheets/manage-tags";
|
||||
import { Button } from "../ui/button";
|
||||
import { ColorTags } from "./color-tags";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const Tags = ({ item, close }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -51,7 +52,7 @@ export const Tags = ({ item, close }) => {
|
||||
buttonType={{
|
||||
text: colors.primary.accent
|
||||
}}
|
||||
title="Add tag"
|
||||
title={strings.addTag()}
|
||||
type="secondary"
|
||||
icon="plus"
|
||||
iconPosition="right"
|
||||
|
||||
@@ -46,6 +46,7 @@ import { MoveNotebookSheet } from "../sheets/move-notebook";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const SelectionHeader = React.memo(
|
||||
({
|
||||
@@ -89,26 +90,24 @@ export const SelectionHeader = React.memo(
|
||||
|
||||
const restoreItem = async () => {
|
||||
if (!selectedItemsList.length) return;
|
||||
await db.trash.restore(...selectedItemsList);
|
||||
if ((await db.trash.restore(...selectedItemsList)) === false) return;
|
||||
Navigation.queueRoutesForUpdate();
|
||||
|
||||
clearSelection();
|
||||
ToastManager.show({
|
||||
heading: "Restore successful",
|
||||
heading: strings.restored(),
|
||||
type: "success"
|
||||
});
|
||||
};
|
||||
|
||||
const deleteItem = async () => {
|
||||
presentDialog({
|
||||
title: `Delete ${selectedItemsList.length > 1 ? "items" : "item"}`,
|
||||
paragraph: `Are you sure you want to delete ${
|
||||
selectedItemsList.length > 1
|
||||
? "these items permanently?"
|
||||
: "this item permanently?"
|
||||
}`,
|
||||
positiveText: "Delete",
|
||||
negativeText: "Cancel",
|
||||
title: strings.doActions.delete[type](selectedItemsList.length),
|
||||
paragraph: strings.actionConfirmations.delete[type](
|
||||
selectedItemsList.length
|
||||
),
|
||||
positiveText: strings.delete(),
|
||||
negativeText: strings.cancel(),
|
||||
positivePress: async () => {
|
||||
if (!selectedItemsList.length) return;
|
||||
await db.trash.delete(...selectedItemsList);
|
||||
@@ -236,10 +235,7 @@ export const SelectionHeader = React.memo(
|
||||
>
|
||||
{[
|
||||
{
|
||||
title:
|
||||
selectedItemsList.length > 1
|
||||
? "Move notebooks"
|
||||
: "Move notebook",
|
||||
title: strings.move(),
|
||||
onPress: async () => {
|
||||
const ids = selectedItemsList;
|
||||
const notebooks = await db.notebooks.all.items(ids);
|
||||
@@ -249,7 +245,7 @@ export const SelectionHeader = React.memo(
|
||||
icon: "arrow-right-bold-box-outline"
|
||||
},
|
||||
{
|
||||
title: "Manage tags",
|
||||
title: strings.manageTags(),
|
||||
onPress: async () => {
|
||||
await sleep(100);
|
||||
ManageTagsSheet.present(selectedItemsList);
|
||||
@@ -258,7 +254,7 @@ export const SelectionHeader = React.memo(
|
||||
icon: "pound"
|
||||
},
|
||||
{
|
||||
title: "Export",
|
||||
title: strings.export(),
|
||||
onPress: async () => {
|
||||
await sleep(100);
|
||||
ExportNotesSheet.present(selectedItemsList);
|
||||
@@ -267,7 +263,7 @@ export const SelectionHeader = React.memo(
|
||||
icon: "export"
|
||||
},
|
||||
{
|
||||
title: "Link notebook",
|
||||
title: strings.linkNotebook(),
|
||||
onPress: async () => {
|
||||
await sleep(100);
|
||||
MoveNoteSheet.present();
|
||||
@@ -276,7 +272,7 @@ export const SelectionHeader = React.memo(
|
||||
icon: "plus"
|
||||
},
|
||||
{
|
||||
title: "Unlink notebook",
|
||||
title: strings.unlinkNotebook(),
|
||||
onPress: async () => {
|
||||
if (!id) return;
|
||||
await db.notes.removeFromNotebook(id, ...selectedItemsList);
|
||||
@@ -288,13 +284,13 @@ export const SelectionHeader = React.memo(
|
||||
icon: "minus"
|
||||
},
|
||||
{
|
||||
title: "Unfavorite",
|
||||
title: strings.unfavorite(),
|
||||
onPress: addToFavorite,
|
||||
visible: focusedRouteId === "Favorites",
|
||||
icon: "star-off"
|
||||
},
|
||||
{
|
||||
title: `Move to trash`,
|
||||
title: strings.moveToTrash(),
|
||||
onPress: async () => {
|
||||
deleteItems(
|
||||
undefined,
|
||||
@@ -308,13 +304,13 @@ export const SelectionHeader = React.memo(
|
||||
icon: "delete"
|
||||
},
|
||||
{
|
||||
title: `Restore`,
|
||||
title: strings.restore(),
|
||||
onPress: restoreItem,
|
||||
visible: type === "trash",
|
||||
icon: "delete-restore"
|
||||
},
|
||||
{
|
||||
title: `Delete`,
|
||||
title: strings.delete(),
|
||||
onPress: deleteItem,
|
||||
visible: type === "trash",
|
||||
icon: "delete"
|
||||
|
||||
@@ -40,6 +40,7 @@ import { MoveNotes } from "../move-notes/movenote";
|
||||
import { eOnNotebookUpdated } from "../../../utils/events";
|
||||
import { getParentNotebookId } from "../../../utils/notebooks";
|
||||
import { useNotebookStore } from "../../../stores/use-notebook-store";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const AddNotebookSheet = ({
|
||||
notebook,
|
||||
@@ -62,25 +63,25 @@ export const AddNotebookSheet = ({
|
||||
|
||||
const onSaveChanges = async () => {
|
||||
if (loading) return;
|
||||
setLoading(true);
|
||||
|
||||
if (!title.current || title?.current.trim().length === 0) {
|
||||
ToastManager.show({
|
||||
heading: "Notebook title is required",
|
||||
heading: strings.enterNotebookTitle(),
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
const id = await db.notebooks.add({
|
||||
title: title.current,
|
||||
description: description.current,
|
||||
id: notebook?.id
|
||||
});
|
||||
|
||||
if (parentNotebook) {
|
||||
if (parentNotebook && id) {
|
||||
await db.relations.add(parentNotebook, {
|
||||
type: "notebook",
|
||||
id: id
|
||||
@@ -93,7 +94,8 @@ export const AddNotebookSheet = ({
|
||||
useNotebookStore.getState().refresh();
|
||||
|
||||
const parent =
|
||||
parentNotebook?.id || (await getParentNotebookId(notebook?.id || id));
|
||||
parentNotebook?.id ||
|
||||
(await getParentNotebookId(notebook?.id || (id as string)));
|
||||
|
||||
eSendEvent(eOnNotebookUpdated, parent);
|
||||
if (notebook) {
|
||||
@@ -103,7 +105,7 @@ export const AddNotebookSheet = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (!notebook && showMoveNotesOnComplete) {
|
||||
if (!notebook && showMoveNotesOnComplete && id) {
|
||||
MoveNotes.present(await db.notebooks.notebook(id));
|
||||
} else {
|
||||
close?.(true);
|
||||
@@ -119,7 +121,7 @@ export const AddNotebookSheet = ({
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>
|
||||
{notebook ? "Edit Notebook" : "New Notebook"}
|
||||
{notebook ? strings.editNotebook() : strings.newNotebook()}
|
||||
</Heading>
|
||||
|
||||
<Seperator />
|
||||
@@ -135,7 +137,7 @@ export const AddNotebookSheet = ({
|
||||
titleInput?.current?.focus();
|
||||
});
|
||||
}}
|
||||
placeholder="Enter notebook title"
|
||||
placeholder={strings.enterNotebookTitle()}
|
||||
onSubmit={() => {
|
||||
descriptionInput.current?.focus();
|
||||
}}
|
||||
@@ -150,14 +152,14 @@ export const AddNotebookSheet = ({
|
||||
onChangeText={(value) => {
|
||||
description.current = value;
|
||||
}}
|
||||
placeholder="Enter notebook description"
|
||||
returnKeyLabel="Next"
|
||||
placeholder={strings.enterNotebookDescription()}
|
||||
returnKeyLabel={strings.next()}
|
||||
returnKeyType="next"
|
||||
defaultValue={notebook ? notebook.description : ""}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={notebook ? "Save" : "Add"}
|
||||
title={notebook ? strings.save() : strings.add()}
|
||||
type="accent"
|
||||
height={45}
|
||||
fontSize={SIZE.md}
|
||||
|
||||
@@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Note } from "@notesnook/core";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, {
|
||||
RefObject,
|
||||
@@ -49,12 +50,11 @@ import DialogHeader from "../../dialog/dialog-header";
|
||||
import SheetProvider from "../../sheet-provider";
|
||||
import { Button } from "../../ui/button";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import Input from "../../ui/input";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { AddNotebookSheet } from "../add-notebook";
|
||||
import { NotebookItem } from "./notebook-item";
|
||||
import { useNotebookItemSelectionStore } from "./store";
|
||||
import { AddNotebookSheet } from "../add-notebook";
|
||||
import Input from "../../ui/input";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
|
||||
async function updateInitialSelectionState(items: string[]) {
|
||||
const relations = await db.relations
|
||||
@@ -200,11 +200,11 @@ const MoveNoteSheet = ({
|
||||
minHeight: 10,
|
||||
flexShrink: 1
|
||||
}}
|
||||
title="Select notebooks"
|
||||
title={strings.selectNotebooks()}
|
||||
paragraph={
|
||||
!multiSelect
|
||||
? "Long press to enable multi-select."
|
||||
: "Select notebooks you want to add note(s) to."
|
||||
? strings.enableMultiSelect()
|
||||
: strings.selectNotebooksDesc()
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -236,7 +236,7 @@ const MoveNoteSheet = ({
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
title="Save"
|
||||
title={strings.save()}
|
||||
type={"accent"}
|
||||
onPress={onSave}
|
||||
/>
|
||||
@@ -266,7 +266,7 @@ const MoveNoteSheet = ({
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
placeholder="Search notebooks...."
|
||||
placeholder={strings.searchNotebooks()}
|
||||
button={{
|
||||
icon: "plus",
|
||||
onPress: () => {
|
||||
@@ -319,7 +319,7 @@ const MoveNoteSheet = ({
|
||||
/>
|
||||
) : (
|
||||
<Paragraph color={colors.primary.icon}>
|
||||
No notebooks
|
||||
{strings.emptyPlaceholders("notebook")}
|
||||
</Paragraph>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React from "react";
|
||||
import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import usePricingPlans from "../../../hooks/use-pricing-plans";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { Button } from "../../ui/button";
|
||||
import dayjs from "dayjs";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
|
||||
|
||||
export const BuyPlan = (props: {
|
||||
planId: string;
|
||||
productId: string;
|
||||
canActivateTrial?: boolean;
|
||||
goBack: () => void;
|
||||
goNext: () => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const pricingPlans = usePricingPlans({
|
||||
planId: props.planId,
|
||||
productId: props.productId,
|
||||
onBuy: () => {
|
||||
props.goNext();
|
||||
}
|
||||
});
|
||||
|
||||
const billingDuration = pricingPlans.getBillingDuration(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription,
|
||||
0,
|
||||
0,
|
||||
true
|
||||
);
|
||||
const is5YearPlanSelected =
|
||||
pricingPlans.selectedProduct?.productId.includes("5year");
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
gap: 16,
|
||||
paddingBottom: 80,
|
||||
paddingTop: Platform.OS === "android" ? insets.top : 0
|
||||
}}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
stickyHeaderIndices={[0]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="chevron-left"
|
||||
onPress={() => {
|
||||
props.goBack();
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
gap: 16
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
{props.canActivateTrial
|
||||
? `Try ${pricingPlans.currentPlan?.name} plan for free`
|
||||
: `${pricingPlans.currentPlan?.name} plan`}{" "}
|
||||
</Heading>
|
||||
|
||||
{props.canActivateTrial ? (
|
||||
<View
|
||||
style={{
|
||||
gap: 10,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
{(is5YearPlanSelected
|
||||
? [
|
||||
"One time purchase, no auto-renewal",
|
||||
"Pay once and use for 5 years"
|
||||
]
|
||||
: [
|
||||
`Free ${billingDuration.duration} day trial, cancel any time`,
|
||||
"Google will remind you before your trial ends"
|
||||
]
|
||||
).map((item) => (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
}}
|
||||
key={item}
|
||||
>
|
||||
<Icon
|
||||
color={colors.primary.accent}
|
||||
size={SIZE.lg}
|
||||
name="check"
|
||||
/>
|
||||
<Paragraph>{item}</Paragraph>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{[
|
||||
`notesnook.${props.planId}.yearly`,
|
||||
`notesnook.${props.planId}.monthly`,
|
||||
...(props.planId === "essential"
|
||||
? []
|
||||
: [`notesnook.${props.planId}.5year`])
|
||||
].map((item) => (
|
||||
<ProductItem
|
||||
key={item}
|
||||
pricingPlans={pricingPlans}
|
||||
productId={item}
|
||||
/>
|
||||
))}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 20
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.primary.paragraph} size={SIZE.sm}>
|
||||
Due today{" "}
|
||||
{pricingPlans.userCanRequestTrial ? (
|
||||
<Text
|
||||
style={{
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
({billingDuration.duration} days free)
|
||||
</Text>
|
||||
) : null}
|
||||
</Heading>
|
||||
|
||||
<Paragraph color={colors.primary.paragraph}>
|
||||
{pricingPlans.userCanRequestTrial
|
||||
? "0.00"
|
||||
: pricingPlans.getStandardPrice(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{pricingPlans.userCanRequestTrial ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
Due{" "}
|
||||
{dayjs().add(billingDuration.duration, "day").format("DD MMMM")}
|
||||
</Paragraph>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{pricingPlans.getStandardPrice(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
width="100%"
|
||||
type="accent"
|
||||
loading={pricingPlans.loading}
|
||||
title={
|
||||
pricingPlans.selectedProduct?.productId.includes("5year")
|
||||
? "Purchase"
|
||||
: pricingPlans?.userCanRequestTrial
|
||||
? "Subscribe and start free trial"
|
||||
: "Subscribe"
|
||||
}
|
||||
onPress={() => {
|
||||
const offerToken = pricingPlans.getOfferTokenAndroid(
|
||||
pricingPlans.selectedProduct as RNIap.SubscriptionAndroid,
|
||||
0
|
||||
);
|
||||
pricingPlans.subscribe(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription,
|
||||
offerToken
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<View>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
{is5YearPlanSelected
|
||||
? `This is a one time purchase, no subscription.`
|
||||
: `Cancel anytime, subscription auto-renews.`}
|
||||
</Heading>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
By joining you agree to our{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy");
|
||||
}}
|
||||
>
|
||||
privacy policy
|
||||
</Text>{" "}
|
||||
and{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos");
|
||||
}}
|
||||
>
|
||||
terms of use
|
||||
</Text>
|
||||
.
|
||||
</Heading>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const ProductItem = (props: {
|
||||
pricingPlans: ReturnType<typeof usePricingPlans>;
|
||||
productId: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const product =
|
||||
props.pricingPlans?.currentPlan?.subscriptions?.[props.productId] ||
|
||||
props.pricingPlans?.currentPlan?.products?.[props.productId];
|
||||
const isAnnual = product?.productId.includes("yearly");
|
||||
const isSelected =
|
||||
product?.productId === props.pricingPlans.selectedProduct?.productId;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: 10
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
props.pricingPlans.selectProduct(product?.productId);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={isSelected ? "radiobox-marked" : "radiobox-blank"}
|
||||
color={isSelected ? colors.primary.accent : colors.secondary.icon}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>
|
||||
{isAnnual
|
||||
? "Yearly"
|
||||
: product?.productId.includes("5year")
|
||||
? "5 year plan (One time purchase)"
|
||||
: "Monthly"}
|
||||
</Heading>
|
||||
|
||||
{isAnnual ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.static.red,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 6,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.static.white} size={SIZE.xs}>
|
||||
Best value -{" "}
|
||||
{props.pricingPlans.compareProductPrice(
|
||||
props.pricingPlans.currentPlan?.id as string,
|
||||
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
|
||||
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
|
||||
)}
|
||||
% Off
|
||||
</Heading>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<Paragraph>
|
||||
{product?.productId.includes("5year")
|
||||
? (product as RNIap.Product).localizedPrice
|
||||
: props.pricingPlans.getStandardPrice(
|
||||
product as RNIap.Subscription
|
||||
)}{" "}
|
||||
{isAnnual || product?.productId.includes("5year")
|
||||
? `(${props.pricingPlans.getPrice(
|
||||
product as RNIap.Subscription,
|
||||
props.pricingPlans.hasTrialOffer() ? 1 : 0,
|
||||
isAnnual
|
||||
)}/month)`
|
||||
: null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -30,6 +30,7 @@ import DialogHeader from "../../dialog/dialog-header";
|
||||
import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import { eUserLoggedIn } from "../../../utils/events";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type ChangeEmailProps = {
|
||||
actionSheetRef: RefObject<ActionSheetRef>;
|
||||
@@ -68,7 +69,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
|
||||
const verified = await db.user?.verifyPassword(
|
||||
emailChangeData.current.password
|
||||
);
|
||||
if (!verified) throw new Error("Password is incorrect");
|
||||
if (!verified) throw new Error(strings.passwordIncorrect());
|
||||
await db.user?.sendVerificationEmail(emailChangeData.current.email);
|
||||
setStep(EmailChangeSteps.changeEmail);
|
||||
setLoading(false);
|
||||
@@ -88,8 +89,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
|
||||
eSendEvent(eUserLoggedIn);
|
||||
close?.();
|
||||
ToastManager.show({
|
||||
heading: `Email changed`,
|
||||
message: `Your account email has been updated to ${emailChangeData.current.email}`,
|
||||
heading: strings.emailUpdated(emailChangeData.current.email),
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
@@ -103,8 +103,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
|
||||
return (
|
||||
<View style={{ paddingHorizontal: 12 }}>
|
||||
<DialogHeader
|
||||
title="Change email"
|
||||
paragraph="Your account email will be changed without affecting your subscription or any other settings."
|
||||
title={strings.changeEmail()}
|
||||
paragraph={strings.changeEmailDesc()}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
@@ -115,7 +115,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
|
||||
<>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
placeholder="Enter your new email"
|
||||
placeholder={strings.enterNewEmail()}
|
||||
validationType="email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
onChangeText={(email) => {
|
||||
@@ -124,7 +124,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
|
||||
/>
|
||||
<Input
|
||||
fwdRef={passInputRef}
|
||||
placeholder="Enter your account password"
|
||||
placeholder={strings.enterAccountPassword()}
|
||||
secureTextEntry
|
||||
onChangeText={(pass) => {
|
||||
emailChangeData.current.password = pass;
|
||||
@@ -135,7 +135,7 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
|
||||
<>
|
||||
<Input
|
||||
fwdRef={codeInputRef}
|
||||
placeholder="Enter verification code sent to your new email"
|
||||
placeholder={strings.verifyNewEmail()}
|
||||
onChangeText={(code) => {
|
||||
emailChangeData.current.code = code;
|
||||
}}
|
||||
@@ -149,8 +149,8 @@ export const ChangeEmail = ({ close }: ChangeEmailProps) => {
|
||||
loading
|
||||
? undefined
|
||||
: step === EmailChangeSteps.verify
|
||||
? "Verify"
|
||||
: "Change email"
|
||||
? strings.verify()
|
||||
: strings.changeEmail()
|
||||
}
|
||||
type="accent"
|
||||
width={250}
|
||||
|
||||
@@ -35,6 +35,7 @@ import { IconButton } from "../../ui/icon-button";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
type TabItem = {
|
||||
id: number;
|
||||
@@ -137,7 +138,9 @@ const TabItemComponent = (props: {
|
||||
numberOfLines={1}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{props.tab.noteId ? item?.title || "Untitled note" : "New note"}
|
||||
{props.tab.noteId
|
||||
? item?.title || strings.untitledNote()
|
||||
: strings.newNote()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
@@ -229,7 +232,7 @@ export default function EditorTabs({
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.lg}>Tabs</Heading>
|
||||
<Heading size={SIZE.lg}>{strings.tabs()}</Heading>
|
||||
<Button
|
||||
onPress={() => {
|
||||
useTabStore.getState().newTab();
|
||||
@@ -240,7 +243,7 @@ export default function EditorTabs({
|
||||
}, 500);
|
||||
close?.();
|
||||
}}
|
||||
title="New tab"
|
||||
title={strings.newTab()}
|
||||
icon="plus"
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
|
||||
@@ -52,6 +52,7 @@ import { Pressable } from "../../ui/pressable";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
const ExportNotesSheet = ({
|
||||
ids,
|
||||
@@ -118,7 +119,6 @@ const ExportNotesSheet = ({
|
||||
await exportNoteAs("pdf");
|
||||
},
|
||||
icon: "file-pdf-box",
|
||||
desc: "View in any pdf reader app",
|
||||
id: notesnook.ids.dialogs.export.pdf,
|
||||
pro: premium
|
||||
},
|
||||
@@ -128,7 +128,6 @@ const ExportNotesSheet = ({
|
||||
await exportNoteAs("md");
|
||||
},
|
||||
icon: "language-markdown",
|
||||
desc: "View in any text or markdown editor",
|
||||
id: notesnook.ids.dialogs.export.md,
|
||||
pro: premium
|
||||
},
|
||||
@@ -138,7 +137,6 @@ const ExportNotesSheet = ({
|
||||
await exportNoteAs("md-frontmatter");
|
||||
},
|
||||
icon: "language-markdown",
|
||||
desc: "View in any text or markdown editor",
|
||||
id: notesnook.ids.dialogs.export.md,
|
||||
pro: premium
|
||||
},
|
||||
@@ -148,7 +146,6 @@ const ExportNotesSheet = ({
|
||||
await exportNoteAs("txt");
|
||||
},
|
||||
icon: "card-text",
|
||||
desc: "View in any text editor",
|
||||
id: notesnook.ids.dialogs.export.text,
|
||||
pro: true
|
||||
},
|
||||
@@ -158,7 +155,6 @@ const ExportNotesSheet = ({
|
||||
await exportNoteAs("html");
|
||||
},
|
||||
icon: "language-html5",
|
||||
desc: "View in any web browser & html reader",
|
||||
id: notesnook.ids.dialogs.export.html,
|
||||
pro: premium
|
||||
}
|
||||
@@ -175,14 +171,7 @@ const ExportNotesSheet = ({
|
||||
>
|
||||
<DialogHeader
|
||||
icon="export"
|
||||
title={
|
||||
ids.length > 1 ? `Export ${ids.length} Notes` : "Export Note"
|
||||
}
|
||||
paragraph={`All exports are saved in ${
|
||||
Platform.OS === "android"
|
||||
? "the selected"
|
||||
: "Notesnook/exported"
|
||||
} folder in phone storage`}
|
||||
title={strings.exportNotes(ids.length)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -238,13 +227,13 @@ const ExportNotesSheet = ({
|
||||
<Heading style={{ marginLeft: 10 }} size={SIZE.md}>
|
||||
{item.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
{/* <Paragraph
|
||||
style={{ marginLeft: 10 }}
|
||||
size={SIZE.sm}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{item.desc}
|
||||
</Paragraph>
|
||||
</Paragraph> */}
|
||||
</View>
|
||||
</Pressable>
|
||||
</Fragment>
|
||||
@@ -264,9 +253,10 @@ const ExportNotesSheet = ({
|
||||
<>
|
||||
<ActivityIndicator />
|
||||
<Paragraph>
|
||||
{`${
|
||||
status ? `${status})` : `Exporting notes`
|
||||
}... Please wait`}
|
||||
{strings.exportingNotes(status) +
|
||||
"..." +
|
||||
" " +
|
||||
strings.pleaseWait()}
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
@@ -287,21 +277,20 @@ const ExportNotesSheet = ({
|
||||
}}
|
||||
color={colors.secondary.heading}
|
||||
>
|
||||
{ids.length > 1
|
||||
? `${ids.length} Notes exported`
|
||||
: "Note exported"}
|
||||
{strings.exportSuccessHeading(ids.length)}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Your {ids.length > 1 ? "notes are" : "note is"} exported
|
||||
successfully as {result?.fileName}
|
||||
{strings.exportSuccessDesc(result?.fileName as string)}
|
||||
</Paragraph>
|
||||
<Button
|
||||
title={
|
||||
Platform.OS === "android" ? "Open file location" : "Open"
|
||||
Platform.OS === "android"
|
||||
? strings.openFileLocation()
|
||||
: strings.open()
|
||||
}
|
||||
type="accent"
|
||||
width={250}
|
||||
@@ -322,8 +311,7 @@ const ExportNotesSheet = ({
|
||||
showAppsSuggestions: true
|
||||
}).catch((e) => {
|
||||
ToastManager.show({
|
||||
heading: "Cannot open",
|
||||
message: `No application found to open ${result.name} file.`,
|
||||
heading: strings.noApplicationFound(result.name),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
@@ -332,7 +320,7 @@ const ExportNotesSheet = ({
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="Share"
|
||||
title={strings.share()}
|
||||
type="secondaryAccented"
|
||||
width={250}
|
||||
fontSize={SIZE.md}
|
||||
@@ -356,7 +344,7 @@ const ExportNotesSheet = ({
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="Export in another format"
|
||||
title={strings.exportAgain()}
|
||||
type="inverted"
|
||||
width={250}
|
||||
fontSize={SIZE.md}
|
||||
|
||||
@@ -23,6 +23,7 @@ import FileViewer from "react-native-file-viewer";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button } from "../../ui/button";
|
||||
import { strings } from "@notesnook/intl";
|
||||
export const ShareComponent = ({ uri, name, padding }) => {
|
||||
return (
|
||||
<View
|
||||
@@ -31,7 +32,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
title="Open"
|
||||
title={strings.open()}
|
||||
type="accent"
|
||||
width="100%"
|
||||
fontSize={SIZE.md}
|
||||
@@ -41,8 +42,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
|
||||
showAppsSuggestions: true
|
||||
}).catch(() => {
|
||||
ToastManager.show({
|
||||
heading: "Cannot open",
|
||||
message: `No application found to open ${name} file.`,
|
||||
heading: strings.noApplicationFound(name),
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
@@ -50,7 +50,7 @@ export const ShareComponent = ({ uri, name, padding }) => {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="Share"
|
||||
title={strings.share()}
|
||||
type="shade"
|
||||
width="100%"
|
||||
fontSize={SIZE.md}
|
||||
|
||||
@@ -34,6 +34,7 @@ import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -75,8 +76,7 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "An error occured",
|
||||
message: e.message,
|
||||
heading: e.message,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
@@ -99,14 +99,14 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Heading>Issue submitted</Heading>
|
||||
<Heading>{strings.issueCreatedHeading()}</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
selectable={true}
|
||||
>
|
||||
You can track your issue at{" "}
|
||||
{strings.issueCreatedDesc[0]()}
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
@@ -118,12 +118,11 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
>
|
||||
{issueUrl.current}
|
||||
</Paragraph>
|
||||
. Please note that we will respond to your issue on the given
|
||||
link. We recommend that you save it.
|
||||
. {strings.issueCreatedDesc[1]()}
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title="Open issue"
|
||||
title={strings.openIssue()}
|
||||
onPress={() => {
|
||||
Linking.openURL(issueUrl.current);
|
||||
}}
|
||||
@@ -135,18 +134,14 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
) : (
|
||||
<>
|
||||
<DialogHeader
|
||||
title={issueTitle || "Report issue"}
|
||||
paragraph={
|
||||
issueTitle
|
||||
? "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap."
|
||||
: "Let us know if you have faced any issue/bug while using Notesnook."
|
||||
}
|
||||
title={issueTitle || strings.issueTitle()}
|
||||
paragraph={issueTitle ? strings.issueDesc() : strings.issueDesc2()}
|
||||
/>
|
||||
|
||||
<Seperator half />
|
||||
|
||||
<TextInput
|
||||
placeholder="Title"
|
||||
placeholder={strings.title()}
|
||||
onChangeText={(v) => (title.current = v)}
|
||||
defaultValue={title.current}
|
||||
style={{
|
||||
@@ -165,13 +160,7 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
<TextInput
|
||||
ref={bodyRef}
|
||||
multiline
|
||||
placeholder={`Tell us more about the issue you are facing.
|
||||
|
||||
For example:
|
||||
- What were you trying to do in the app?
|
||||
- What did you expect to happen?
|
||||
- Steps to reproduce the issue
|
||||
- Things you have tried etc.`}
|
||||
placeholder={strings.issuePlaceholder()}
|
||||
numberOfLines={5}
|
||||
textAlignVertical="top"
|
||||
onChangeText={(v) => (body.current = v)}
|
||||
@@ -211,7 +200,7 @@ For example:
|
||||
<Seperator />
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={loading ? null : "Submit"}
|
||||
title={loading ? null : strings.submit()}
|
||||
loading={loading}
|
||||
width="100%"
|
||||
type="accent"
|
||||
@@ -225,10 +214,10 @@ For example:
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
The information above will be publically available at{" "}
|
||||
{strings.issueNotice[0]()}{" "}
|
||||
<Text
|
||||
onPress={() => {
|
||||
Linking.openURL("https://github.com/streetwriters/notesnook");
|
||||
Linking.openURL("https://github.com/streetwriters/notesnook/issues");
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
@@ -237,8 +226,7 @@ For example:
|
||||
>
|
||||
github.com/streetwriters/notesnook.
|
||||
</Text>{" "}
|
||||
If you want to ask something in general or need some assistance, we
|
||||
would suggest that you{" "}
|
||||
{strings.issueNotice[1]()}{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
@@ -255,7 +243,7 @@ For example:
|
||||
}
|
||||
}}
|
||||
>
|
||||
join our community on Discord.
|
||||
{strings.issueNotice[2]()}
|
||||
</Text>
|
||||
</Paragraph>
|
||||
</>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user