Compare commits

..

71 Commits

Author SHA1 Message Date
Ammar Ahmed
33ea52068b mobile: add line break when appending to note 2025-05-17 12:14:22 +05:00
Ammar Ahmed
3e86167842 mobile: fix crash in settings -> user subscription 2025-05-17 10:05:35 +05:00
01zulfi
93713d58d4 web: persist double spaced lines config on logout (#8097)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-16 15:21:26 +05:00
01zulfi
7da18de68c web: fix readonly note not locking when vault is locked (#8092)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-16 15:21:10 +05:00
01zulfi
fccf2a7b3e web: add support for drag/dropping notes on tab strip (#8090)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-16 15:20:28 +05:00
Abdullah Atta
b200180684 web: bump version to 3.2.0-beta.2 2025-05-16 15:05:40 +05:00
Abdullah Atta
0a5ea5d448 core: fix `expected previously executed migration 2 to be at index 1 but
10 was found in its place.`
2025-05-16 15:04:27 +05:00
01zulfi
739906dbe4 core: fix db.settings.getTrashCleanupInterval return type (#8076)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-16 14:48:28 +05:00
01zulfi
4326552ccc web: add support for removing recents in command palette using delete key (#8069)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-16 14:44:11 +05:00
01zulfi
1ee1ceef73 web: press shift+enter to create note in quick open (#8068)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-16 14:43:24 +05:00
Ammar Ahmed
4d83666fed editor: fix alignment items in callout heading (#8101)
* editor: fix alignment items in callout heading

* Update packages/editor/styles/styles.css

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

---------

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
2025-05-16 14:36:29 +05:00
Abdullah Atta
86b8a2ae8c desktop: update patched better-sqlite3-multiple-ciphers to 11.6.1 2025-05-16 12:52:11 +05:00
Abdullah Atta
c8c21d210b global: regenerate package-lock.json for theme & ui packages 2025-05-16 11:22:21 +05:00
Ammar Ahmed
96343a3b1f mobile: fix error in build 2025-05-16 11:03:16 +05:00
Ammar Ahmed
74ebc4c82e mobile: add option to disable auto update check 2025-05-16 10:58:15 +05:00
Abdullah Atta
3acd3a87a9 misc: include @notesnook/theme package when generating flatpak sources 2025-05-16 10:24:54 +05:00
Abdullah Atta
94888ad94d desktop: downgrade playwright 2025-05-16 10:15:21 +05:00
Abdullah Atta
030daf12f2 desktop: fix flatpak sources generation 2025-05-15 12:17:28 +05:00
Abdullah Atta
d599a0836f desktop: regenerate package-lock.json 2025-05-15 10:36:42 +05:00
Abdullah Atta
7f901e7e75 web: update package-lock.json 2025-05-15 10:28:54 +05:00
Abdullah Atta
0a5e235e50 global: fix dependencies version differences 2025-05-15 09:59:39 +05:00
Abdullah Atta
aec3530d8a global: update package-lock.json 2025-05-15 09:47:41 +05:00
01zulfi
732e79dd60 web: fix list pane not expanding (#7963)
* web: fix list pane not expanding
* fix list pane not expanding to saved width on reload
* fix list pane not expanding to saved width on collapse

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: collapse pane and set size to 0 when applicable
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

---------

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-15 09:25:29 +05:00
01zulfi
ca5c596158 core: segregate logic for note headline and note headline title generation (#7968)
* core: segregate logic for note headline and note headline title generation
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: minor refactor note headline title
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-15 09:25:29 +05:00
Ammar Ahmed
9e8e9c54d4 ci: fix build ios
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-05-15 09:25:29 +05:00
Ammar Ahmed
2c2a79104b ci: fix build ios
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-05-15 09:25:29 +05:00
Ammar Ahmed
27769f8682 ci: fix build ios
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-05-15 09:25:29 +05:00
Ammar Ahmed
f150637e3a mobile: fix crash when going from trash -> search 2025-05-15 09:25:29 +05:00
Ammar Ahmed
f04661e32a mobile: release v3.1.1 2025-05-15 09:25:29 +05:00
Ammar Ahmed
db65e80dbc mobile: fix crash on delete item 2025-05-15 09:25:29 +05:00
Ammar Ahmed
4e65bbb7bf mobile: fix crash in list 2025-05-15 09:25:29 +05:00
Ammar Ahmed
d5ec302bf6 mobile: fix cursor bug on ios 2025-05-15 09:25:29 +05:00
Ammar Ahmed
51692a67e7 mobile: fix typing in editor title input 2025-05-15 09:25:29 +05:00
Ammar Ahmed
ee97d71f09 mobile: archive notes 2025-05-12 12:17:29 +05:00
Abdullah Atta
6d089c7332 web: bump version to 3.2.0-beta.1 2025-05-06 13:34:16 +05:00
Abdullah Atta
ab1b45bd50 core: fix query-transformer tests 2025-05-06 13:28:24 +05:00
Ammar Ahmed
fff8b61f71 mobile: add sqlite3-fts5-html module 2025-05-06 13:28:24 +05:00
Abdullah Atta
2c4f262fbc desktop: install sqlite3-fts5-html 2025-05-06 13:28:24 +05:00
Abdullah Atta
e5a7e08efb web: update wa-sqlite to use sqlite3-fts5-html for stripping html 2025-05-06 13:28:24 +05:00
Abdullah Atta
b4ef01c688 core: fix short queries search 2025-05-06 13:28:24 +05:00
Abdullah Atta
e6d6194b66 core: remove @orama/stopwords 2025-05-06 13:28:24 +05:00
Abdullah Atta
b6ee0989e5 core: strip html when searching notes
this commit also properly fixes search queries containing
words less than 3 characters in length
2025-05-06 13:28:24 +05:00
Abdullah Atta
e4230a0709 core: fix warnings in tests 2025-05-06 13:28:24 +05:00
Ammar Ahmed
0b17588e5d mobile: update better trigram 2025-05-06 13:28:24 +05:00
Abdullah Atta
8498f35b95 core: fix db migration 2025-05-06 13:28:24 +05:00
Abdullah Atta
8eecc995fc core: wrap query with invalid symbols in quotes 2025-05-06 13:28:24 +05:00
Abdullah Atta
1bbc01346b core: update sqlite-better-trigram to v0.0.3 2025-05-06 13:28:24 +05:00
01zulfi
2f3a895ddc core: fix trash cleanup interval setting retrieval (#8045)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-02 12:15:00 +05:00
01zulfi
5caa18543a web: add more archive test cases (#8040)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-02 10:11:53 +05:00
Abdullah Atta
33c493b317 desktop: no need to externalize @resvg/resvg-js 2025-05-02 10:11:08 +05:00
Abdullah Atta
ca497f3815 desktop: remove icon from native menu 2025-05-02 10:11:08 +05:00
Abdullah Atta
d796795c40 desktop: only show native menus on macos 2025-05-02 10:11:08 +05:00
Abdullah Atta
5c9d98d5d6 desktop: show native menus where possible 2025-05-02 10:11:08 +05:00
01zulfi
3b47d97314 web: assign accent color to links (#8041)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-01 13:14:40 +05:00
Abdullah Atta
4360d4b155 desktop: update packages 2025-05-01 13:13:38 +05:00
01zulfi
24d8bfe9bd desktop: upgrade electron to v34.0.0 (#8038)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-01 10:23:54 +05:00
01zulfi
832506b836 web: fix automatic backup reminder setting reverting to never (#8037)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-05-01 09:07:30 +05:00
01zulfi
795f38b4b8 web: fix focus last active tab method (#8034)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-04-30 11:30:24 +05:00
01zulfi
d42813b262 editor: improve hover link popup position (#8011)
* move it down so it doesn't block the link

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-04-29 09:32:38 +05:00
Abdullah Atta
d522c65d49 web: bump version to 3.2.0-beta.0 2025-04-28 11:18:25 +05:00
Abdullah Atta
4b7cecccbe web: update @notesnook-imoporter/core to 2.2.2 2025-04-28 11:09:51 +05:00
01zulfi
151abb94e1 web: fix notestore is not defined error (#8012)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-04-28 11:09:05 +05:00
Abdullah Atta
b67a307453 web: update @notesnook-importer/core to 2.2.1 2025-04-28 10:31:54 +05:00
Abdullah Atta
bf83c87911 web: update @notesnook-importer/core to 2.2.0 2025-04-28 09:59:57 +05:00
Abdullah Atta
8e67d3dc23 web: open settings with importer page instead of importer.notesnook.com 2025-04-28 09:59:57 +05:00
Ammar Ahmed
4b24f2c4b6 mobile: fix default font setting not followed 2025-04-28 09:59:57 +05:00
01zulfi
6cd14a5a78 web: make nav menu header draggable (#7985)
* web: make part of nav menu header draggable
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* Update apps/web/src/app.css

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

* Update apps/web/src/components/navigation-menu/index.tsx

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>

---------

Signed-off-by: Abdullah Atta <thecodrr@protonmail.com>
Co-authored-by: Abdullah Atta <thecodrr@protonmail.com>
2025-04-28 09:59:57 +05:00
Ammar Ahmed
6edae50960 mobile: remove READ_MEDIA_IMAGES permission 2025-04-28 09:59:57 +05:00
Abdullah Atta
2a6a377d06 web: refresh app after import 2025-04-28 09:59:57 +05:00
Abdullah Atta
c569dbd778 web: move importer into web/desktop app (#7472)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-04-28 09:59:57 +05:00
01zulfi
36022df189 web: add support for archiving notes (#7810)
* web: archive notes
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* global: arhives -> archived
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-04-28 09:45:59 +05:00
719 changed files with 239655 additions and 796758 deletions

View File

@@ -7,7 +7,7 @@ runs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22.x
node-version: 20.x
cache: "npm"
cache-dependency-path: |
package-lock.json

View File

@@ -47,11 +47,6 @@ jobs:
- name: Install Detox CLI
run: npm install detox-cli --global
- name: Check for typescript errors
run: |
cd apps/mobile
npx tsc --noEmit
- name: Detox build
run: |
yarn build:android

View File

@@ -69,16 +69,12 @@ jobs:
run: |
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
- name: CCache Stats Before Build
run: ccache -sv
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Build unsigned app bundle
run: yarn release:android:bundle

View File

@@ -69,16 +69,12 @@ jobs:
run: |
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
sed -i -e 's/arguments/arguments "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache",/g' apps/mobile/node_modules/react-native-reanimated/android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-reanimated//android/build.gradle
sed -i -e 's/defaultConfig {/ndkVersion safeExtGet('ndkVersion', "25.2.9519653")\n defaultConfig {/g' apps/mobile/node_modules/react-native-mmkv-storage/android/build.gradle
- name: CCache Stats Before Build
run: ccache -sv
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
- name: Build unsigned app bundle
run: yarn release:android:bundle

View File

@@ -9,13 +9,7 @@ on:
- "packages/core/**"
# re-run workflow if workflow file changes
- ".github/workflows/core.tests.yml"
pull_request_target:
branches:
- "master"
paths:
- "packages/core/**"
# re-run workflow if workflow file changes
- ".github/workflows/core.tests.yml"
pull_request:
types:
- "ready_for_review"
- "opened"
@@ -23,21 +17,11 @@ on:
- "reopened"
jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo true
test:
needs: authorize
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -52,6 +36,7 @@ jobs:
echo "USER_PASSWORD=${{ secrets.USER_PASSWORD }}" >> $GITHUB_ENV
echo "USER_TOTP_SECRET=${{ secrets.USER_TOTP_SECRET }}" >> $GITHUB_ENV
echo "USER_HASHED_PASSWORD=${{ secrets.USER_HASHED_PASSWORD }}" >> $GITHUB_ENV
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Run all @notesnook/core tests
run: npm run tx @notesnook/core:test:e2e
run: npx nx test:e2e @notesnook/core

View File

@@ -45,7 +45,7 @@ on:
jobs:
build:
name: Build
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -65,13 +65,17 @@ jobs:
npm i --cpu x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Generate desktop build (stable)
if: ${{ inputs.release-track == 'stable' }}
run: npm run tx @notesnook/web:build:desktop
run: npx nx build:desktop @notesnook/web
- name: Generate desktop build (beta)
if: ${{ inputs.release-track == 'beta' }}
run: BETA=true npm run tx @notesnook/web:build:desktop
run: BETA=true npx nx build:desktop @notesnook/web
- name: Build desktop bundle
working-directory: ./apps/desktop
@@ -103,8 +107,7 @@ jobs:
name: Notesnook Desktop v${{ steps.app_metadata.outputs.app_version }}
tag_name: v${{ steps.app_metadata.outputs.app_version }}
files: ./notesnook_build_v${{ steps.app_metadata.outputs.app_version }}.zip
prerelease: ${{ inputs.release-track == 'beta' }}
target_commitish: ${{ github.ref }}
prerelease: ${{ endsWith(steps.app_metadata.outputs.app_version, '-beta') }}
- name: Generate flatpak sources
if: inputs.publish-github && inputs.build-linux
@@ -195,9 +198,9 @@ jobs:
CSC_LINK: ${{ secrets.mac_certs }}
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
run: |
npm run tx @notesnook/desktop:release -- --variant=mas
cd apps/desktop
npx nx run release --project @notesnook/desktop -- --variant=mas
yarn electron-builder --config=electron-builder.config.js --mac mas --universal -p never
working-directory: ./apps/desktop
- name: Build zip and dmg
env:
@@ -208,13 +211,13 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
run: |
npm run tx @notesnook/desktop:release
cd apps/desktop
npx nx run release --project @notesnook/desktop
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p always
else
yarn electron-builder --config=electron-builder.config.js --mac zip dmg --arm64 --x64 -p never
fi
working-directory: ./apps/desktop
- name: Deploy to Testflight
if: inputs.publish-apple && steps.appstore.outputs.app-version-latest != steps.app_metadata.outputs.app_version
@@ -229,8 +232,8 @@ jobs:
xcrun altool --upload-package $package -t osx --apiKey $API_KEY_ID --apiIssuer $API_KEY_ISSUER_ID --apple-id ${{ steps.app_metadata.outputs.apple_app_id }} --bundle-id ${{ steps.app_metadata.outputs.app_bundle_id }} --bundle-short-version-string ${{ steps.app_metadata.outputs.app_version }} --bundle-version ${{ steps.app_metadata.outputs.bundle_version }}
working-directory: ./apps/desktop
build-linux-x64:
name: Build for Linux x64
build-linux:
name: Build for Linux
needs: build
if: inputs.build-linux
runs-on: ubuntu-22.04
@@ -247,76 +250,9 @@ jobs:
run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
- name: Install Snapcraft
uses: samuelmeuli/action-snapcraft@v3
uses: samuelmeuli/action-snapcraft@v1
if: inputs.publish-snap
- 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 x64 sqlite-better-trigram
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
- name: Build snap
if: inputs.publish-snap
run: |
yarn electron-builder --config=electron-builder.config.js --linux snap:x64 -p never
working-directory: ./apps/desktop
- name: Build AppImage
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p always
else
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 -p never
fi
working-directory: ./apps/desktop
- name: Publish on Snapcraft
if: inputs.publish-snap
run: |
if [ ${{ inputs.release-track }} == 'beta' ]; then
snapcraft upload --release=beta ./output/notesnook_linux_amd64.snap
else
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
fi
working-directory: ./apps/desktop
build-linux-arm64:
name: Build for Linux arm64
needs: build
if: inputs.build-linux
runs-on: ubuntu-22.04-arm
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
# - name: Setup Snapcraft Auth
# if: inputs.publish-snap
# run: echo "SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.snapcraft_token }}" >> $GITHUB_ENV
# - name: Install Snapcraft
# uses: samuelmeuli/action-snapcraft@v1
# if: inputs.publish-snap
- name: Download build
uses: actions/download-artifact@v4
with:
@@ -331,33 +267,35 @@ jobs:
- 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: npm run tx @notesnook/desktop:release
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
# - name: Build snap
# if: inputs.publish-snap
# run: |
# yarn electron-builder --config=electron-builder.config.js --linux snap:arm64 -p never
# working-directory: ./apps/desktop
- name: Build snap
if: inputs.publish-snap
run: |
yarn electron-builder --config=electron-builder.config.js --linux snap:x64 -p never
working-directory: ./apps/desktop
- name: Build AppImage
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ ${{ inputs.publish-github }} == true ]; then
yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p always
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 -p always
else
yarn electron-builder --config=electron-builder.config.js --linux AppImage:arm64 -p never
yarn electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 -p never
fi
working-directory: ./apps/desktop
# - name: Publish on Snapcraft
# if: inputs.publish-snap
# run: |
# snapcraft upload --release=stable ./output/notesnook_linux_arm64.snap
# working-directory: ./apps/desktop
- name: Publish on Snapcraft
if: inputs.publish-snap
run: |
snapcraft upload --release=stable ./output/notesnook_linux_amd64.snap
working-directory: ./apps/desktop
build-windows:
name: Build for Windows
@@ -396,13 +334,10 @@ jobs:
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
npm i --cpu arm64 sqlite3-fts5-html
npm i --cpu x64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Build
run: node scripts/execute.mjs @notesnook/desktop:release
run: npx nx run release --project @notesnook/desktop
- name: Publish
env:

View File

@@ -6,16 +6,10 @@ on:
branches:
- "master"
paths:
- "apps/desktop/**"
- "app/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
pull_request:
branches:
- "master"
paths:
- "apps/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
jobs:
build:
@@ -33,8 +27,12 @@ jobs:
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: npm run tx @notesnook/web:build:desktop
run: npx nx build:desktop @notesnook/web
- name: Archive build artifact
uses: actions/upload-artifact@v4
@@ -66,7 +64,8 @@ jobs:
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
@@ -109,7 +108,8 @@ jobs:
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
@@ -158,7 +158,8 @@ jobs:
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npm run tx @notesnook/desktop:release
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |
@@ -204,13 +205,11 @@ jobs:
run: |
npm i --cpu arm64 sqlite-better-trigram
npm i --cpu x64 sqlite-better-trigram
npm i --cpu arm64 sqlite3-fts5-html
npm i --cpu x64 sqlite3-fts5-html
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: node scripts/execute.mjs @notesnook/desktop:release
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
- name: Build app
run: |

View File

@@ -10,12 +10,6 @@ on:
# re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml"
pull_request:
branches:
- "master"
paths:
- "packages/editor/**"
# re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml"
types:
- "ready_for_review"
- "opened"
@@ -38,8 +32,12 @@ jobs:
npm ci --ignore-scripts --prefer-offline --no-audit
npm run bootstrap -- --scope=editor
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
- name: Build editor
run: npm run tx @notesnook/editor:build
run: npx nx build @notesnook/editor
- name: Run all @notesnook/editor tests
run: npm run tx @notesnook/editor:test
run: npx nx test @notesnook/editor

View File

@@ -25,7 +25,7 @@ jobs:
npm run bootstrap -- --scope=mobile
- name: Build packages
run: npm run tx @notesnook/mobile:build
run: npx nx run @notesnook/mobile:build
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.11
@@ -59,16 +59,7 @@ jobs:
key: ${{ runner.os }}-pods-${{ hashFiles('apps/mobile/native/ios/Podfile.lock') }}
- name: Install Pods
run: |
cd apps/mobile/native/ios
bundle install
RCT_NEW_ARCH_ENABLED=0 bundle exec pod install
- name: Check for typescript errors
run: |
npm run tx mobile:build
cd apps/mobile
npx tsc --noEmit
run: npm run prepare:ios
- name: CCache Stats Before Build
run: ccache -sv

View File

@@ -39,13 +39,13 @@ jobs:
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
- name: Generate build
run: npm run tx @notesnook/monograph:build
run: npx nx build @notesnook/monograph
# Setup Buildx
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
- name: Log in to Docker Hub
uses: docker/login-action@v3
@@ -77,7 +77,7 @@ jobs:
context: apps/monograph
file: apps/monograph/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
cache-from: streetwriters/monograph:latest

View File

@@ -20,6 +20,7 @@ jobs:
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV

View File

@@ -20,6 +20,7 @@ jobs:
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV

View File

@@ -27,6 +27,10 @@ jobs:
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

View File

@@ -30,6 +30,7 @@ jobs:
- name: Setup environment
run: |
echo "NX_CLOUD_ACCESS_TOKEN=${{ secrets.NX_CLOUD_ACCESS_TOKEN }}" >> $GITHUB_ENV
echo "CLOUDFLARE_ACCOUNT_ID=${{ secrets.CLOUDFLARE_ACCOUNT_ID }}" >> $GITHUB_ENV
echo "CLOUDFLARE_API_TOKEN=${{ secrets.CLOUDFLARE_API_TOKEN }}" >> $GITHUB_ENV

View File

@@ -9,13 +9,7 @@ on:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.tests.yml"
pull_request_target:
branches:
- "master"
paths:
- "apps/web/**"
# re-run workflow if workflow file changes
- ".github/workflows/web.tests.yml"
pull_request:
types:
- "ready_for_review"
- "opened"
@@ -23,23 +17,12 @@ on:
- "reopened"
jobs:
authorize:
environment: ${{ github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository &&
'external' || 'internal' }}
runs-on: ubuntu-latest
steps:
- run: echo true
build:
needs: authorize
name: Build
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Setup Node
uses: ./.github/actions/setup-node-with-cache
@@ -49,6 +32,10 @@ jobs:
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 test build
run: npm run build:test:web
@@ -67,8 +54,6 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
- name: Download build
uses: actions/download-artifact@v4

3
.gitignore vendored
View File

@@ -11,5 +11,4 @@ nx-cloud.env
site
node_modules.backup
.nx
/*.patch
.taskcache
*.patch

View File

@@ -27,6 +27,7 @@ Notesnook is built using the following technologies:
3. React Native — For mobile apps we are using React Native
4. Electron — For desktop app
5. NPM — listed here because we **don't** use Yarn or PNPM or XYZ across any of our projects.
6. Nx — maintaining monorepos is hard but Nx makes it easier.
> **Note: Each project in the monorepo contains its own architecture details which you can refer to.**
@@ -65,7 +66,7 @@ We take all queries, issues and bug reports that you might have. Feel free to as
## Additional Resources
- [Migrating & Importing your data from other apps — Importer](https://help.notesnook.com/importing-notes)
- [Migrating & Importing your data from other apps — Importer](https://importer.notesnook.com/)
- [Privacy policy](https://notesnook.com/privacy) & [Terms of service](https://notesnook.com/terms)
- [Verify Notesnook encryption claims yourself — Vericrypt](https://vericrypt.notesnook.com/)
- [Why Notesnook requires an email address?](https://blog.notesnook.com/why-notesnook-requires-an-email-address/)

View File

@@ -24,7 +24,7 @@ Requirements:
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
1. `clone` the monorepo:
Once you have completed the setup, the first step is to `clone` the monorepo:
```bash
git clone https://github.com/streetwriters/notesnook.git
@@ -33,28 +33,19 @@ git clone https://github.com/streetwriters/notesnook.git
cd notesnook
```
2. Install dependencies:
Once you are inside the `./notesnook` directory, run the preparation step:
```bash
# this might take a while to complete
npm install
```
3. Run the webapp for desktop environment:
```bash
cd apps/web
npm run start:desktop
```
4. In a separate terminal session, run the desktop app from the root of the project:
Now you can finally start the desktop app for development:
```bash
npm run start:desktop
```
### Release mode
To run the app in release mode:
```bash
@@ -67,10 +58,10 @@ This will compile and run the app in production mode but it won't generate any p
npm run release -- --rebuild
# For macOS
npx electron-builder --config=electron-builder.config.js --mac dmg --arm64 --x64 --publish never
npx electron-builder --config=electron-builder.config.js --mac dmg --arm64 --x64 -publish never
# For Linux (AppImage)
npx electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 --publish never
npx electron-builder --config=electron-builder.config.js --linux AppImage:x64 AppImage:arm64 -publish never
# For Windows
npx electron-builder --config=electron-builder.config.js --win --publish never

View File

@@ -17,226 +17,230 @@ 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 { testCleanup, test } from "./test-override.js";
import { test } from "vitest";
import { harness } from "./utils.js";
import { writeFile } from "fs/promises";
import { Page } from "playwright";
import { gt, lt } from "semver";
import { describe } from "vitest";
test("update starts downloading if version is outdated", async ({
ctx: { page },
expect,
onTestFinished
}) => {
onTestFinished(testCleanup);
test("update starts downloading if version is outdated", async (t) => {
await harness(
t,
async ({ page }) => {
await page.waitForSelector("#authForm");
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /updating/i })
.waitFor({ state: "attached" });
});
test("update is only shown if version is outdated and auto updates are disabled", async ({
ctx,
expect,
onTestFinished
}) => {
onTestFinished(testCleanup);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false
})
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /updating/i })
.waitFor({ state: "attached" });
},
{ version: "3.0.0" }
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.waitFor({ state: "attached" });
});
describe("update to stable if it is newer", () => {
test.scoped({ options: { version: "3.0.0-beta.0" } });
test("test", async ({ ctx, expect, onTestFinished }) => {
onTestFinished(testCleanup);
test("update is only shown if version is outdated and auto updates are disabled", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false
})
);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch();
await ctx.relaunch();
const { page } = ctx;
const { page } = ctx;
await page.waitForSelector("#authForm");
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page.waitForSelector(".ProseMirror");
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
expect(gt(version, "3.0.0-beta.0")).toBe(true);
});
});
describe("update is not available if it latest stable version is older", () => {
test.scoped({ options: { version: "99.0.0-beta.0" } });
test("test", async ({ ctx, expect, onTestFinished }) => {
onTestFinished(testCleanup);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
expect(
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.isHidden()
).toBe(true);
});
.waitFor({ state: "attached" });
},
{ version: "3.0.0" }
);
});
describe("downgrade to stable on switching to stable release track", () => {
test.scoped({ options: { version: "99.0.0-beta.0" } });
test("test", async ({ ctx, expect, onTestFinished }) => {
onTestFinished(testCleanup);
test("update to stable if it is newer", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "stable"
})
);
await ctx.relaunch();
await ctx.relaunch();
const { page } = ctx;
const { page } = ctx;
await page.waitForSelector("#authForm");
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page.waitForSelector(".ProseMirror");
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
t.expect(gt(version, "3.0.0-beta.0")).toBe(true);
},
{ version: "3.0.0-beta.0" }
);
});
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
test("update is not available if it latest stable version is older", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "beta"
})
);
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
expect(lt(version, "99.0.0-beta.0")).toBe(true);
});
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
t.expect(
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i })
.isHidden()
).toBe(true);
},
{ version: "99.0.0-beta.0" }
);
});
test("downgrade to stable on switching to stable release track", async (t) => {
await harness(
t,
async (ctx) => {
await ctx.app.close();
await writeFile(
ctx.configPath,
JSON.stringify({
automaticUpdates: false,
releaseTrack: "stable"
})
);
await ctx.relaunch();
const { page } = ctx;
await page.waitForSelector("#authForm");
t.expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await skipDialog(page);
await page.waitForSelector(".ProseMirror");
await page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /checking for updates/i })
.waitFor({ state: "hidden" });
const updateButton = page
.locator(".theme-scope-statusBar")
.getByRole("button", { name: /available/i });
await updateButton.waitFor({ state: "visible" });
const content = await updateButton.textContent();
const version = content?.split(" ")?.[0] || "";
t.expect(lt(version, "99.0.0-beta.0")).toBe(true);
},
{ version: "99.0.0-beta.0" }
);
});
async function skipDialog(page: Page) {
try {
const dialog = page.locator(".ReactModal__Content");
const positiveButton = dialog.locator(
"button[data-role='positive-button']"
);
const negativeButton = dialog.locator(
"button[data-role='negative-button']"
);
if (await positiveButton.isVisible())
await positiveButton.click({ timeout: 1000 });
else if (await negativeButton.isVisible())
await negativeButton.click({ timeout: 1000 });
} catch (e) {
// ignore error
}
await page
.waitForSelector(".ReactModal__Content", {
timeout: 1000
})
.catch(() => {})
.then(async () => {
const positiveButton = page.locator(
"button[data-role='positive-button']"
);
const negativeButton = page.locator(
"button[data-role='negative-button']"
);
if (await positiveButton.isVisible()) await positiveButton.click();
else if (await negativeButton.isVisible()) await negativeButton.click();
});
}

View File

@@ -17,24 +17,22 @@ 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 { testCleanup, test } from "./test-override.js";
import { test } from "vitest";
import { harness } from "./utils.js";
import assert from "assert";
test("make sure app loads", async ({
ctx: { page },
expect,
onTestFinished
}) => {
onTestFinished(testCleanup);
test("make sure app loads", async (t) => {
await harness(t, async ({ page }) => {
await page.waitForSelector("#authForm");
await page.waitForSelector("#authForm");
assert.ok(
await page.getByRole("button", { name: "Create account" }).isVisible()
);
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror");
await page.waitForSelector(".ProseMirror");
});
});

View File

@@ -1,64 +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 { test as vitestTest, TestContext } from "vitest";
import { buildAndLaunchApp, Fixtures, TestOptions } from "./utils";
import { mkdir, rm } from "fs/promises";
import path from "path";
import slugify from "slugify";
export const test = vitestTest.extend<Fixtures>({
options: { version: "3.0.0" } as TestOptions,
ctx: async ({ options }, use) => {
const ctx = await buildAndLaunchApp(options);
await use(ctx);
}
});
export async function testCleanup(context: TestContext) {
const ctx = (context.task.context as unknown as Fixtures).ctx;
if (context.task.result?.state === "fail") {
await mkdir("test-results", { recursive: true });
await ctx.page.screenshot({
path: path.join(
"test-results",
`${slugify(context.task.name)}-${process.platform}-${
process.arch
}-error.png`
)
});
}
await ctx.app.close();
await rm(ctx.userDataDir, {
force: true,
recursive: true,
maxRetries: 3,
retryDelay: 5000
}).catch(() => {
/*ignore */
});
await rm(ctx.outputDir, {
force: true,
recursive: true,
maxRetries: 3,
retryDelay: 5000
}).catch(() => {
/*ignore */
});
}

View File

@@ -18,198 +18,144 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { execSync } from "child_process";
import { cp } from "fs/promises";
import { mkdir } from "fs/promises";
import { fileURLToPath } from "node:url";
import path, { join, resolve } from "path";
import path from "path";
import { _electron as electron } from "playwright";
import { existsSync } from "fs";
import slugify from "slugify";
import { TaskContext } from "vitest";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const IS_DEBUG = process.env.NN_DEBUG === "true" || process.env.CI === "true";
const productName = `NotesnookTestHarness`;
const SOURCE_DIR = resolve("output", productName);
export interface AppContext {
interface AppContext {
app: import("playwright").ElectronApplication;
page: import("playwright").Page;
configPath: string;
userDataDir: string;
outputDir: string;
relaunch: () => Promise<void>;
}
export interface TestOptions {
interface TestOptions {
version: string;
}
export interface Fixtures {
options: TestOptions;
ctx: AppContext;
export async function harness(
t: TaskContext,
cb: (ctx: AppContext) => Promise<void>,
options?: TestOptions
) {
const ctx = await buildAndLaunchApp(options);
t.onTestFinished(async (result) => {
if (result.state === "fail") {
await mkdir("test-results", { recursive: true });
await ctx.page.screenshot({
path: path.join(
"test-results",
`${slugify(t.task.name)}-${process.platform}-${
process.arch
}-error.png`
)
});
}
await ctx.app.close();
});
await cb(ctx);
}
export async function buildAndLaunchApp(
options?: TestOptions
): Promise<AppContext> {
const productName = `notesnooktest${makeid(10)}`;
const outputDir = path.join("test-artifacts", `${productName}-output`);
const executablePath = await copyBuild({
...options,
outputDir
});
const { app, page, configPath, userDataDir } = await launchApp(
executablePath,
productName,
options?.version
);
async function buildAndLaunchApp(options?: TestOptions): Promise<AppContext> {
const productName = makeid(10);
const executablePath = await buildApp({ ...options, productName });
const { app, page, configPath } = await launchApp(executablePath);
const ctx: AppContext = {
app,
page,
configPath,
userDataDir,
outputDir,
relaunch: async () => {
const { app, page, configPath, userDataDir } = await launchApp(
executablePath,
productName,
options?.version
);
const { app, page, configPath } = await launchApp(executablePath);
ctx.app = app;
ctx.page = page;
ctx.userDataDir = userDataDir;
ctx.configPath = configPath;
}
};
return ctx;
}
async function launchApp(
executablePath: string,
packageName: string,
version?: string
) {
const userDataDir = resolve(
__dirname,
"..",
"test-artifacts",
"user_data_dirs",
packageName
);
async function launchApp(executablePath: string) {
const app = await electron.launch({
executablePath,
args: IS_DEBUG ? [] : ["--hidden"],
env: {
...(process.platform === "linux"
env:
process.platform === "linux"
? {
...(process.env as Record<string, string>),
APPIMAGE: "true"
}
: (process.env as Record<string, string>)),
CUSTOM_USER_DATA_DIR: userDataDir,
...(version
? {
CUSTOM_APP_VERSION: version
}
: {})
}
: (process.env as Record<string, string>)
});
const page = await app.firstWindow();
const configPath = path.join(userDataDir, "UserData", "config.json");
const userDataDirectory = await app.evaluate((a) => {
return a.app.getPath("userData");
});
const configPath = path.join(userDataDirectory, "config.json");
return {
app,
page,
configPath,
userDataDir
configPath
};
}
let MAX_RETRIES = 3;
export async function buildApp(version?: string) {
if (!existsSync(SOURCE_DIR)) {
const args = [
"electron-builder",
"--dir",
`--${process.arch}`,
`--config electron-builder.config.js`,
`--c.extraMetadata.productName=${productName}`,
`--c.compression=store`,
"--publish=never"
];
if (version) args.push(`--c.extraMetadata.version=${version}`);
try {
execSync(`npx ${args.join(" ")}`, {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
NOTESNOOK_STAGING: "true",
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,
NN_OUTPUT_DIR: SOURCE_DIR
}
});
} catch (e) {
if (--MAX_RETRIES) {
console.log("retrying...");
return await buildApp(version);
} else throw e;
}
}
}
async function copyBuild({ outputDir }: { outputDir: string }) {
return process.platform === "win32"
? await makeBuildCopyWindows(outputDir, productName)
: process.platform === "darwin"
? await makeBuildCopyMacOS(outputDir, productName)
: await makeBuildCopyLinux(outputDir, productName);
}
async function makeBuildCopyLinux(outputDir: string, productName: string) {
const platformDir =
process.arch === "arm64" ? "linux-arm64-unpacked" : "linux-unpacked";
const appDir = await makeBuildCopy(outputDir, platformDir);
return resolve(
__dirname,
"..",
appDir,
productName.toLowerCase().replace(/\s+/g, "-")
);
}
async function makeBuildCopyWindows(outputDir: string, productName: string) {
const platformDir =
process.arch === "arm64" ? "win-arm64-unpacked" : "win-unpacked";
const appDir = await makeBuildCopy(outputDir, platformDir);
return resolve(__dirname, "..", appDir, `${productName}.exe`);
}
async function makeBuildCopyMacOS(outputDir: string, productName: string) {
const platformDir = process.arch === "arm64" ? "mac-arm64" : "mac";
const appDir = await makeBuildCopy(outputDir, platformDir);
return resolve(
__dirname,
"..",
appDir,
`${productName}.app`,
"Contents",
"MacOS",
productName
);
}
async function makeBuildCopy(outputDir: string, platformDir: string) {
const appDir = outputDir;
await cp(join(SOURCE_DIR, platformDir), outputDir, {
recursive: true,
preserveTimestamps: true,
verbatimSymlinks: true,
dereference: false,
force: true
async function buildApp({
version,
productName
}: {
version?: string;
productName: string;
}) {
const buildRoot = path.join("test-artifacts", `${productName}-build`);
const output = path.join("test-artifacts", `${productName}-output`);
execSync(`npm run release -- --root ${buildRoot} --skip-tsc-build`, {
stdio: IS_DEBUG ? "inherit" : "ignore"
});
return appDir;
const args = [
`--config electron-builder.config.js`,
`--c.extraMetadata.productName=${productName}`,
"--publish=never"
];
if (version) args.push(`--c.extraMetadata.version=${version}`);
execSync(`npx electron-builder --dir --${process.arch} ${args.join(" ")}`, {
stdio: IS_DEBUG ? "inherit" : "ignore",
env: {
...process.env,
NOTESNOOK_STAGING: "true",
NN_BUILD_ROOT: buildRoot,
NN_PRODUCT_NAME: productName,
NN_APP_ID: `com.notesnook.test.${productName}`,
NN_OUTPUT_DIR: output
}
});
return path.join(
__dirname,
"..",
output,
process.platform === "linux"
? process.arch === "arm64"
? "linux-arm64-unpacked"
: "linux-unpacked"
: process.platform === "darwin"
? process.arch === "arm64"
? `mac-arm64/${productName}.app/Contents/MacOS/`
: `mac/${productName}.app/Contents/MacOS/`
: "win-unpacked",
process.platform === "win32" ? `${productName}.exe` : productName
);
}
function makeid(length: number) {

View File

@@ -38,20 +38,13 @@ const linuxExecutableName = process.env.NN_PRODUCT_NAME
const year = new Date().getFullYear();
const isBeta = pkg.version.includes("-beta");
/**
* @type {import("app-builder-lib").Configuration}
*/
module.exports = {
appId: appId,
productName: productName,
copyright: `Copyright © ${year} Streetwriters (Private) Limited`,
artifactName: "notesnook_${os}_${arch}.${ext}",
generateUpdatesFilesForAllChannels: true,
asar: true,
asarUnpack: [
"node_modules/sqlite-better-trigram-@(linux|darwin|windows)-${arch}/**/*",
"node_modules/sqlite3-fts5-html-@(linux|darwin|windows)-${arch}/**/*"
],
asar: false,
files: [
"!*.chunk.js.map",
"!*.chunk.js.LICENSE.txt",
@@ -201,8 +194,7 @@ module.exports = {
snap: {
autoStart: false,
confinement: "strict",
allowNativeWayland: true,
base: "core22"
allowNativeWayland: true
},
extraResources: ["app-update.yml", "./assets/**"],
extraMetadata: {

File diff suppressed because it is too large Load Diff

View File

@@ -2,15 +2,13 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.3.5",
"version": "3.2.0-beta.2",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"sideEffects": [
"src/overrides.ts"
],
"sideEffects": false,
"exports": {
".": {
"require": {
@@ -32,12 +30,12 @@
"@notesnook/ui": "file:../../packages/ui",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"better-sqlite3-multiple-ciphers": "^12.4.1",
"better-sqlite3-multiple-ciphers": "11.3.0",
"electron-trpc": "0.7.1",
"electron-updater": "^6.6.2",
"icojs": "^0.19.5",
"sqlite-better-trigram": "0.0.3",
"sqlite3-fts5-html": "^0.0.4",
"sqlite3-fts5-html": "^0.0.2",
"typed-emitter": "^2.1.0",
"yargs": "^17.7.2",
"zod": "3.24.3"
@@ -47,7 +45,7 @@
"@types/node": "22.15.3",
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.3",
"electron": "^37.0.0",
"electron": "^34.0.0",
"electron-builder": "^26.0.12",
"esbuild": "0.21.5",
"node-abi": "^4.5.0",
@@ -57,7 +55,7 @@
"slugify": "1.6.6",
"tree-kill": "^1.2.2",
"undici": "^7.8.0",
"vitest": "^3.2.4"
"vitest": "2.1.8"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"

View File

@@ -1,13 +0,0 @@
diff --git a/node_modules/app-builder-lib/out/targets/snap.js b/node_modules/app-builder-lib/out/targets/snap.js
index 0b5405e..753f6c6 100644
--- a/node_modules/app-builder-lib/out/targets/snap.js
+++ b/node_modules/app-builder-lib/out/targets/snap.js
@@ -115,7 +115,7 @@ class SnapTarget extends core_1.Target {
else {
const archTriplet = archNameToTriplet(arch);
appDescriptor.environment = {
- DISABLE_WAYLAND: options.allowNativeWayland ? "" : "1",
+ DISABLE_WAYLAND: options.allowNativeWayland ? "0" : "1",
PATH: "$SNAP/usr/sbin:$SNAP/usr/bin:$SNAP/sbin:$SNAP/bin:$PATH",
SNAP_DESKTOP_RUNTIME: "$SNAP/gnome-platform",
LD_LIBRARY_PATH: [

View File

@@ -1,11 +0,0 @@
diff --git a/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi b/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
index 1a14ecd..ff938f1 100644
--- a/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
+++ b/node_modules/better-sqlite3-multiple-ciphers/deps/defines.gypi
@@ -38,5 +38,6 @@
'SQLITE_TRACE_SIZE_LIMIT=32',
'SQLITE_USER_AUTHENTICATION=0',
'SQLITE_USE_URI=0',
+ 'SQLITE_ENABLE_REGEXP',
],
}

View File

@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
console.log("starting build...");
import path from "path";
import fs from "fs/promises";
import { existsSync } from "fs";
@@ -27,7 +26,6 @@ import * as childProcess from "child_process";
import { fileURLToPath } from "url";
import { patchBetterSQLite3 } from "./patch-better-sqlite3.mjs";
console.log("imports done...");
const args = yargs(process.argv);
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -36,27 +34,17 @@ const skipTscBuild = args.skipTscBuild || false;
const webAppPath = path.resolve(path.join(__dirname, "..", "..", "web"));
console.log("loaded args", {
args,
__filename,
__dirname,
root,
skipTscBuild,
webAppPath
});
await fs.rm(path.join(root, "build"), { force: true, recursive: true });
console.log("removed build folder");
if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
console.log("rebuilding...");
await exec(
"node scripts/execute.mjs @notesnook/web:build:desktop",
"npx nx build:desktop @notesnook/web",
path.join(__dirname, "..", "..", "..")
);
}
await patchBetterSQLite3();
// temporary until there's support for prebuilt binaries for linux ARM
if (os.platform() === "linux") await patchBetterSQLite3();
await fs.cp(path.join(webAppPath, "build"), path.join(root, "build"), {
recursive: true,

View File

@@ -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/>.
*/
import { readFile, rm, writeFile } from "fs/promises";
import { readFile, writeFile } from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
@@ -25,8 +25,6 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function patchBetterSQLite3() {
console.log("Patching better-sqlite3");
const jsonPath = path.join(
__dirname,
"..",
@@ -36,23 +34,15 @@ export async function patchBetterSQLite3() {
);
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
delete json.homepage;
delete json.repository;
json.version = "11.6.1";
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
json.repository.url =
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
await writeFile(jsonPath, JSON.stringify(json));
await rm(
path.join(
__dirname,
"..",
"node_modules",
"better-sqlite3-multiple-ciphers",
"build"
),
{ force: true, recursive: true }
);
}
if (process.argv[1] === __filename) {
console.log("Patching better-sqlite3");
patchBetterSQLite3();
}

View File

@@ -33,8 +33,6 @@ export class SQLite {
initialized = false;
preparedStatements: Map<string, Statement<unknown[]>> = new Map();
retryCounter: Record<string, number> = {};
extensionsLoaded = false;
constructor() {
console.log("new sqlite worker");
}
@@ -48,6 +46,10 @@ export class SQLite {
this.sqlite = require("better-sqlite3-multiple-ciphers")(
filePath
).unsafeMode(true);
const betterTrigram = require("sqlite-better-trigram");
const fts5Html = require("sqlite3-fts5-html");
betterTrigram.load(this.sqlite);
fts5Html.load(this.sqlite);
}
/**
@@ -115,29 +117,9 @@ export class SQLite {
} catch (e) {
if (e instanceof Error) e.message += ` (query: ${sql})`;
throw e;
} finally {
// Since SQLite 3.48.0 (SQLite3MC v2.0.2) it's not possible to load fts5
// extensions before database has been decrypting. This is because
// executing a `SELECT` now accesses the underlying databases resulting in
// an error. Since FTS5 extensions depend on `SELECT fts5` to load the
// fts5 API, we must wait decrypt the database before we can load
// the extensions.
if (!this.extensionsLoaded && (await this.isDatabaseReady())) {
this.loadExtensions();
}
}
}
private loadExtensions() {
this.sqlite?.loadExtension(
getExtensionPath("sqlite-better-trigram", "better-trigram")
);
this.sqlite?.loadExtension(
getExtensionPath("sqlite3-fts5-html", "fts5-html")
);
this.extensionsLoaded = true;
}
async run<R>(
sql: string,
parameters?: SQLiteCompatibleType[]
@@ -162,53 +144,4 @@ export class SQLite {
retryDelay: 500
});
}
/**
* This just executes `SELECT 1` on the database to make sure its ready.
* On an encrypted database, this will fail until `PRAGMA key` has been
* called.
*/
private async isDatabaseReady() {
// return this.exec(`SELECT 1;`)
// .then(() => true)
// .catch(() => false);
if (!this.sqlite) return false;
try {
this.sqlite.prepare(`SELECT 1;`).run();
return true;
} catch {
return false;
}
}
}
function getExtensionPath(extensionName: string, entryPoint: string) {
const path = require("path");
const { statSync } = require("fs");
const os = process.platform === "win32" ? "windows" : process.platform;
const packageName = `${extensionName}-${os}-${process.arch}`;
const extensionSuffix =
process.platform === "win32"
? "dll"
: process.platform === "darwin"
? "dylib"
: "so";
let loadablePath = path.join(
require.resolve(extensionName),
"..",
"..",
packageName,
`${entryPoint}.${extensionSuffix}`
);
if (loadablePath.includes(".asar"))
loadablePath = loadablePath
.replace("electron.asar", "app.asar")
.replace(".asar", ".asar.unpacked");
if (!statSync(loadablePath, { throwIfNoEntry: false })) {
throw new Error(`${extensionName} not found at ${loadablePath}.`);
}
return loadablePath;
}

View File

@@ -17,8 +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 "./overrides";
import { app, BrowserWindow, nativeTheme, shell, dialog } from "electron";
import { app, BrowserWindow, nativeTheme, shell } from "electron";
import { isDevelopment } from "./utils";
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";
import { configureAutoUpdater } from "./utils/autoupdater";
@@ -182,17 +181,6 @@ async function createWindow() {
app.once("ready", async () => {
console.info("App ready. Opening window.");
if (app.runningUnderARM64Translation) {
console.log("App is running under ARM64 translation");
dialog.showMessageBoxSync({
message:
"Notesnook detected that it is running under ARM64 translation. For the best performance, please download the ARM64 build of Notesnook from our website.",
type: "warning",
buttons: ["Okay"],
title: "Degraded Performance Warning"
});
}
if (config.customDns) enableCustomDns();
else disableCustomDns();

View File

@@ -1,41 +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 { app } from "electron";
import path from "path";
const customVersion = process.env.CUSTOM_APP_VERSION;
if (customVersion) {
app.getVersion = () => customVersion;
console.log("setting custom version:", customVersion);
}
if (process.env.CUSTOM_USER_DATA_DIR) {
app.setPath(
"appData",
path.join(process.env.CUSTOM_USER_DATA_DIR, "AppData")
);
app.setPath(
"userData",
path.join(process.env.CUSTOM_USER_DATA_DIR, "UserData")
);
app.setPath(
"documents",
path.join(process.env.CUSTOM_USER_DATA_DIR, "Documents")
);
}

View File

@@ -21,13 +21,11 @@ import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
testTimeout: 120 * 1000,
hookTimeout: 120 * 1000,
testTimeout: process.env.CI ? 120 * 1000 : 120 * 1000,
sequence: {
concurrent: true,
shuffle: true
},
globalSetup: "./__tests__/global-setup.ts",
dir: "./__tests__/",
exclude: [
"**/node_modules/**",

View File

@@ -69,7 +69,6 @@ buck-out/
*/fastlane/report.xml
*/fastlane/Preview.html
*/fastlane/screenshots
native/vendor
# Bundle artifact
*.jsbundle

File diff suppressed because one or more lines are too long

View File

@@ -94,9 +94,7 @@ export async function encryptDatabaseKeyWithPassword(appLockPassword: string) {
}
export async function restoreDatabaseKeyToKeyChain(appLockPassword: string) {
const databaseKeyCipher: Cipher = CipherStorage.getMap(
DB_KEY_CIPHER
) as Cipher;
const databaseKeyCipher: Cipher = CipherStorage.getMap(DB_KEY_CIPHER);
const databaseKey = (await decrypt(
{
password: appLockPassword
@@ -137,9 +135,7 @@ export async function clearAppLockVerificationCipher() {
export async function validateAppLockPassword(appLockPassword: string) {
try {
const appLockCipher: Cipher = CipherStorage.getMap(
APPLOCK_CIPHER
) as Cipher;
const appLockCipher: Cipher = CipherStorage.getMap(APPLOCK_CIPHER);
if (!appLockCipher) return true;
const key = await Sodium.deriveKey(appLockPassword, appLockCipher.salt);
const decrypted = await decrypt(key, appLockCipher);
@@ -163,9 +159,7 @@ export function clearDatabaseKey() {
export async function getDatabaseKey(appLockPassword?: string) {
if (DB_KEY) return DB_KEY;
if (appLockPassword) {
const databaseKeyCipher: Cipher = CipherStorage.getMap(
"databaseKeyCipher"
) as Cipher;
const databaseKeyCipher: Cipher = CipherStorage.getMap("databaseKeyCipher");
const databaseKey = await decrypt(
{
password: appLockPassword
@@ -299,7 +293,7 @@ export async function deriveCryptoKey(data: SerializedKey) {
export async function getCryptoKey() {
try {
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER) as Cipher;
const keyCipher: Cipher = MMKV.getMap(USER_KEY_CIPHER);
if (!keyCipher) {
DatabaseLogger.info("User key cipher is null");
return undefined;

View File

@@ -16,7 +16,7 @@ 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 { database, getFeature, getFeatureLimit } from "@notesnook/common";
import { database } from "@notesnook/common";
import { logger as dbLogger, ICompressor } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
@@ -26,6 +26,7 @@ import {
} from "@streetwriters/kysely";
import { Platform } from "react-native";
import * as Gzip from "react-native-gzip";
import SettingsService from "../../services/settings";
import EventSource from "../../utils/sse/even-source-ios";
import AndroidEventSource from "../../utils/sse/event-source";
import { FileStorage } from "../filesystem";
@@ -33,24 +34,11 @@ import { getDatabaseKey } from "./encryption";
import "./logger";
import { RNSqliteDriver } from "./sqlite.kysely";
import { Storage } from "./storage";
import SettingsService from "../../services/settings";
export async function setupDatabase(password?: string) {
const key = await getDatabaseKey(password);
if (!key) throw new Error(strings.databaseSetupFailed());
// const base = `http://192.168.100.92`;
// database.host({
// API_HOST: `${base}:5264`,
// AUTH_HOST: `${base}:8264`,
// SSE_HOST: `${base}:7264`,
// ISSUES_HOST: `${base}:2624`,
// SUBSCRIPTIONS_HOST: `${base}:9264`,
// MONOGRAPH_HOST: `${base}:6264`,
// NOTESNOOK_HOST: `${base}:8788`
// });
database.host({
API_HOST: "https://api.notesnook.com",
AUTH_HOST: "https://auth.streetwriters.co",
@@ -58,7 +46,6 @@ export async function setupDatabase(password?: string) {
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
ISSUES_HOST: "https://issues.streetwriters.co",
MONOGRAPH_HOST: "https://monogr.ph",
NOTESNOOK_HOST: "https://notesnook.com",
...(SettingsService.getProperty("serverUrls") || {})
});
@@ -86,10 +73,6 @@ export async function setupDatabase(password?: string) {
tempStore: "memory",
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL",
password: key
},
maxNoteVersions: async () => {
const limit = await getFeatureLimit(getFeature("maxNoteVersions"));
return typeof limit.caption === "number" ? limit.caption : undefined;
}
});
}

View File

@@ -136,12 +136,6 @@ export const Storage: IStorage = {
clear(): Promise<void> {
return DefaultStorage.clear();
},
generateCryptoKeyPair() {
throw new Error("Not implemented");
},
decryptAsymmetric() {
throw new Error("Not implemented");
},
getAllKeys(): Promise<string[]> {
return DefaultStorage.getAllKeys();
},

View File

@@ -137,7 +137,6 @@ export async function deleteFile(
export async function clearFileStorage() {
try {
await createCacheDir();
const files = await RNFetchBlob.fs.ls(cacheDir);
const oldCache = await RNFetchBlob.fs.ls(cacheDirOld);
@@ -222,7 +221,6 @@ export async function deleteCacheFileByName(name: string) {
}
export async function deleteDCacheFiles() {
await createCacheDir();
const files = await RNFetchBlob.fs.ls(cacheDir);
for (const file of files) {
if (file.includes("_dcache") || file.startsWith("NN_")) {

View File

@@ -74,6 +74,21 @@ export async function uploadFile(
return true;
}
const uploadUrlResponse = await fetch(url, {
method: "PUT",
headers
});
const uploadUrl = uploadUrlResponse.ok
? await uploadUrlResponse.text()
: await uploadUrlResponse.json();
if (typeof uploadUrl !== "string") {
throw new Error(
uploadUrl.error || "Unable to resolve attachment upload url."
);
}
DatabaseLogger.info(`Starting upload: ${filename}`);
const uploadRequest = RNFetchBlob.config({
@@ -82,9 +97,8 @@ export async function uploadFile(
})
.fetch(
"PUT",
url,
uploadUrl,
{
...headers,
"content-type": ""
},
RNFetchBlob.wrap(filePath)

View File

@@ -25,17 +25,10 @@ import { useSelectionStore } from "../../stores/use-selection-store";
import { allowedOnPlatform, renderItem } from "./functions";
import { getContainerBorder } from "../../utils/colors";
import { DefaultAppStyles } from "../../utils/styles";
import Heading from "../ui/typography/heading";
import { AppFontSize } from "../../utils/size";
import { Button } from "../ui/button";
import { strings } from "@notesnook/intl";
export const Announcement = ({ color }) => {
const { colors } = useThemeColors();
const [announcements, remove] = useMessageStore((state) => [
state.announcements,
state.remove
]);
const announcements = useMessageStore((state) => state.announcements);
let announcement = announcements.length > 0 ? announcements[0] : null;
const selectionMode = useSelectionStore((state) => state.selectionMode);
@@ -64,38 +57,6 @@ export const Announcement = ({ color }) => {
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
paddingBottom: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
<Heading color={colors.secondary.heading} size={AppFontSize.xxs}>
{strings.announcement()}
</Heading>
<Button
type="plain"
icon="close"
height={null}
onPress={() => {
remove(announcement.id);
}}
iconSize={20}
fontSize={AppFontSize.xs}
style={{
paddingVertical: 0,
paddingHorizontal: 0,
zIndex: 10
}}
/>
</View>
{announcement?.body
.filter((item) => allowedOnPlatform(item.platforms))
.map((item, index) =>

View File

@@ -25,6 +25,7 @@ import { eSendEvent, presentSheet } from "../../services/event-manager";
import { eCloseAnnouncementDialog } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { PricingPlans } from "../premium/pricing-plans";
import SheetProvider from "../sheet-provider";
import { Button } from "../ui/button";
import { allowedOnPlatform, getStyle } from "./functions";
@@ -44,6 +45,18 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
Linking.openURL(item.data).catch(() => {
/* empty */
});
} else if (item.type === "promo") {
presentSheet({
component: (
<PricingPlans
marginTop={1}
promo={{
promoCode: item.data,
text: item.title
}}
/>
)
});
}
};
return (
@@ -51,8 +64,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
style={{
paddingHorizontal: DefaultAppStyles.GAP,
...getStyle(style),
flexDirection: inline ? "row" : "column",
gap: DefaultAppStyles.GAP_SMALL
flexDirection: inline ? "row" : "column"
}}
>
<SheetProvider context="premium_cta" />
@@ -87,10 +99,12 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
fontSize={AppFontSize.sm}
type="plain"
onPress={() => onPress(item)}
width={null}
height={30}
style={{
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
alignSelf: "flex-start",
paddingHorizontal: 0
paddingHorizontal: 0,
marginLeft: 12
}}
textStyle={{
textDecorationLine: "underline"
@@ -105,6 +119,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
<Button
key={item.title}
title={item.title}
fontSize={AppFontSize.md}
buttonType={{
color: color ? color : colors.primary.accent,
text: color
@@ -114,8 +129,10 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
opacity: 1
}}
onPress={() => onPress(item)}
width={250}
style={{
width: "100%"
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderRadius: 100
}}
/>
))}
@@ -132,7 +149,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
height={30}
style={{
minWidth: "50%",
width: "100%"
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
textStyle={{
textDecorationLine: "underline"

View File

@@ -21,17 +21,16 @@ import React from "react";
import { AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { getStyle } from "./functions";
import { DefaultAppStyles } from "../../utils/styles";
export const Description = ({ text, style = {}, inline }) => {
return (
<Paragraph
style={{
paddingHorizontal: DefaultAppStyles.GAP,
marginHorizontal: 12,
...getStyle(style),
textAlign: inline ? "left" : style?.textAlign
}}
size={AppFontSize.sm}
size={inline ? AppFontSize.sm : AppFontSize.md}
>
{text}
</Paragraph>

View File

@@ -33,7 +33,6 @@ import {
import BaseDialog from "../dialog/base-dialog";
import { allowedOnPlatform, renderItem } from "./functions";
import { useCallback } from "react";
import { DefaultAppStyles } from "../../utils/styles";
/**
* Test announcement
@@ -139,9 +138,9 @@ export const AnnouncementDialog = () => {
maxHeight: DDS.isTab ? "90%" : "100%",
borderRadius: DDS.isTab ? 10 : 0,
overflow: "hidden",
paddingVertical: DefaultAppStyles.GAP,
borderTopRightRadius: 15,
borderTopLeftRadius: 15
marginBottom: DDS.isTab ? 20 : 0,
borderTopRightRadius: 10,
borderTopLeftRadius: 10
}}
>
<FlatList

View File

@@ -27,6 +27,10 @@ import { getStyle } from "./functions";
import { DefaultAppStyles } from "../../utils/styles";
export const Title = ({ text, style = {}, inline }) => {
const announcements = useMessageStore((state) => state.announcements);
let announcement = announcements.length > 0 ? announcements[0] : null;
const remove = useMessageStore((state) => state.remove);
return inline ? (
<View
style={{
@@ -38,21 +42,46 @@ export const Title = ({ text, style = {}, inline }) => {
>
<Heading
style={{
paddingHorizontal: DefaultAppStyles.GAP,
marginHorizontal: DefaultAppStyles.GAP,
marginTop: DefaultAppStyles.GAP_VERTICAL,
...getStyle(style),
textAlign: inline ? "left" : style?.textAlign,
flexShrink: 1
}}
numberOfLines={1}
size={inline ? AppFontSize.md : AppFontSize.xl}
>
{inline ? text?.toUpperCase() : text}
</Heading>
<Button
type="plain"
icon="close"
height={null}
onPress={() => {
remove(announcement.id);
}}
hitSlop={{
left: 15,
top: 10,
bottom: 10,
right: 0
}}
iconSize={24}
fontSize={AppFontSize.xs}
style={{
borderRadius: 100,
paddingVertical: 0,
paddingHorizontal: 0,
marginRight: 12,
zIndex: 10
}}
/>
</View>
) : (
<Heading
style={{
paddingHorizontal: DefaultAppStyles.GAP,
marginHorizontal: DefaultAppStyles.GAP,
...getStyle(style),
marginTop: style?.marginTop || DefaultAppStyles.GAP_VERTICAL
}}

View File

@@ -336,6 +336,7 @@ const AppLocked = () => {
borderRadius: 150,
marginBottom: 10
}}
fontSize={AppFontSize.md}
/>
</>
) : null}

View File

@@ -167,7 +167,6 @@ const Actions = ({
setAttachments();
eSendEvent(eDBItemUpdate, attachment.id);
}
return true;
},
positiveText: strings.rename()
});

View File

@@ -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,7 +31,9 @@ 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";
@@ -46,6 +51,7 @@ const AuthModal = () => {
const [visible, setVisible] = useState(false);
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
const actionSheetRef = useRef();
const insets = useGlobalSafeAreaInsets();
useEffect(() => {
eSubscribeEvent(eOpenLoginDialog, open);
@@ -93,18 +99,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: DefaultAppStyles.GAP,
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: DefaultAppStyles.GAP_SMALL
}}
/>
)}
</View>
</View>
<Toast context="local" />
</BaseDialog>

View File

@@ -17,20 +17,26 @@ 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 React, { useRef, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import BackupService from "../../services/backup";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../services/event-manager";
import { useUserStore } from "../../stores/use-user-store";
import { eOpenRecoveryKeyDialog } from "../../utils/events";
import { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
import { eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events";
import DialogHeader from "../dialog/dialog-header";
import { Button } from "../ui/button";
import Input from "../ui/input";
import { Notice } from "../ui/notice";
import Seperator from "../ui/seperator";
import { Dialog } from "../dialog";
import BackupService from "../../services/backup";
import { sleep } from "../../utils/time";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
export const ChangePassword = () => {
const passwordInputRef = useRef();
@@ -79,7 +85,8 @@ export const ChangePassword = () => {
context: "global"
});
setLoading(false);
Navigation.goBack();
eSendEvent(eCloseSheet);
await sleep(300);
eSendEvent(eOpenRecoveryKeyDialog);
} catch (e) {
setLoading(false);
@@ -101,6 +108,9 @@ export const ChangePassword = () => {
}}
>
<Dialog context="change-password-dialog" />
<DialogHeader title={strings.changePassword()} />
<Seperator />
<Input
fwdRef={oldPasswordInputRef}
onChangeText={(value) => {
@@ -150,3 +160,9 @@ export const ChangePassword = () => {
</View>
);
};
ChangePassword.present = () => {
presentSheet({
component: <ChangePassword />
});
};

View File

@@ -25,18 +25,13 @@ export const AuthMode = {
login: 0,
signup: 1,
welcomeSignup: 2,
welcomeLogin: 3,
trialSignup: 4
trialSignup: 3
};
export const initialAuthMode = createRef(0);
export function hideAuth(context) {
export function hideAuth() {
eSendEvent(eCloseLoginDialog);
if (
initialAuthMode.current === AuthMode.welcomeSignup ||
initialAuthMode.current === AuthMode.welcomeLogin ||
context === "intro"
) {
if (initialAuthMode.current === AuthMode.welcomeSignup) {
Navigation.replace("FluidPanelsView");
} else {
Navigation.goBack();

View File

@@ -1,78 +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 { useRoute } from "@react-navigation/native";
import React from "react";
import { View } from "react-native";
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 route = useRoute();
return (
<View
style={{
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((route.params as any)?.context);
}}
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>
);
};

View File

@@ -17,29 +17,99 @@ 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, { useState } from "react";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import Navigation from "../../services/navigation";
import { Toast } from "../toast";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import { AuthMode, initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { useThemeColors } from "@notesnook/theme";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { DefaultAppStyles } from "../../utils/styles";
import { useSettingStore } from "../../stores/use-setting-store";
const Auth = ({ navigation, route }) => {
const [currentAuthMode, setCurrentAuthMode] = useState(
route?.params?.mode || AuthMode.login
);
const deviceMode = useSettingStore((state) => state.deviceMode);
const { colors } = useThemeColors();
const insets = useGlobalSafeAreaInsets();
initialAuthMode.current = route?.params.mode || AuthMode.login;
useNavigationFocus(navigation, {});
return (
<SafeAreaView
style={{ flex: 1, backgroundColor: colors.primary.background }}
>
{currentAuthMode !== AuthMode.login &&
currentAuthMode !== AuthMode.welcomeLogin ? (
<View style={{ flex: 1 }}>
<View
style={{
position: "absolute",
paddingTop: insets.top,
top: 0,
zIndex: 999,
backgroundColor:
deviceMode === "mobile" ? colors.secondary.background : null,
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
width: "100%",
height: 50,
justifyContent:
initialAuthMode.current !== AuthMode.welcomeSignup
? "space-between"
: "flex-end"
}}
>
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
<IconButton
name="arrow-left"
onPress={() => {
if (initialAuthMode.current === 2) {
Navigation.replace("FluidPanelsView");
} else {
Navigation.goBack();
}
}}
color={colors.primary.paragraph}
/>
)}
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
<Button
title={strings.skip()}
onPress={() => {
if (initialAuthMode.current === 2) {
Navigation.replace("FluidPanelsView");
} else {
Navigation.goBack();
}
}}
iconSize={16}
type="plain"
iconPosition="right"
icon="chevron-right"
height={25}
iconStyle={{
marginTop: 2
}}
style={{
paddingHorizontal: DefaultAppStyles.GAP_SMALL
}}
/>
)}
</View>
</View>
{currentAuthMode !== AuthMode.login ? (
<Signup
changeMode={(mode) => setCurrentAuthMode(mode)}
trial={AuthMode.trialSignup === currentAuthMode}
@@ -53,7 +123,7 @@ const Auth = ({ navigation, route }) => {
)}
<Toast context="local" />
</SafeAreaView>
</View>
);
};

View File

@@ -19,22 +19,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { useRoute } from "@react-navigation/native";
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 Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import Sync from "../../services/sync";
import { useUserStore } from "../../stores/use-user-store";
import { eUserLoggedIn } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { Dialog } from "../dialog";
import SheetProvider from "../sheet-provider";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button";
import Input from "../ui/input";
@@ -42,9 +37,8 @@ import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { ForgotPassword } from "./forgot-password";
import { AuthHeader } from "./header";
import { useLogin } from "./use-login";
import SettingsService from "../../services/settings";
import { DefaultAppStyles } from "../../utils/styles";
const LoginSteps = {
emailAuth: 1,
@@ -55,7 +49,6 @@ const LoginSteps = {
export const Login = ({ changeMode }) => {
const { colors } = useThemeColors();
const [focused, setFocused] = useState(false);
const route = useRoute();
const {
step,
setStep,
@@ -68,24 +61,15 @@ export const Login = ({ changeMode }) => {
setError,
login
} = useLogin(async () => {
hideAuth();
eSendEvent(eUserLoggedIn, true);
await sleep(500);
hideAuth();
Progress.present();
setTimeout(() => {
if (!useUserStore.getState().syncing) {
Sync.run("global", false, "full");
}
}, 5000);
if (!PremiumService.get() && !SettingsService.getProperty("serverUrls")) {
Navigation.navigate("PayWall", {
context: "signup",
state: route.params.state,
canGoBack: false
});
} else {
Progress.present();
}
});
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
@@ -104,227 +88,217 @@ export const Login = ({ changeMode }) => {
return (
<>
<AuthHeader />
<ForgotPassword />
<Dialog context="two_factor_verify" />
<KeyboardAwareScrollView
<SheetProvider context="two_factor_verify" />
<View
style={{
width: "100%"
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
height: "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%",
height: "100%",
alignSelf: "center"
justifyContent: "flex-end",
paddingHorizontal: DefaultAppStyles.GAP,
backgroundColor: colors.secondary.background,
borderBottomWidth: 0.8,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderBottomColor: colors.primary.border,
alignSelf: isTablet ? "center" : undefined,
borderWidth: isTablet ? 1 : null,
borderColor: isTablet ? colors.primary.border : null,
borderRadius: isTablet ? 20 : null,
marginTop: isTablet ? 50 : null,
width: !isTablet ? null : "70%",
minHeight: height * 0.4
}}
>
<View
style={{
justifyContent: "flex-end",
paddingHorizontal: DefaultAppStyles.GAP,
borderBottomWidth: 0.8,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderBottomColor: colors.primary.border,
alignSelf: isTablet ? "center" : undefined,
borderWidth: isTablet ? 1 : null,
borderColor: isTablet ? colors.primary.border : null,
borderRadius: isTablet ? 20 : null,
marginTop: isTablet ? 50 : null,
width: !isTablet ? 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: DefaultAppStyles.GAP_VERTICAL
}}
extraBold
size={AppFontSize.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: DDS.isTab ? 0 : DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Input
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
testID="input.email"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
autoComplete="email"
validationType="email"
marginBottom={0}
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();
}
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={strings.done()}
returnKeyType="done"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
autoCorrect={false}
placeholder={strings.password()}
marginBottom={0}
editable={!loading}
defaultValue={password.current}
onSubmit={() => login()}
/>
<Button
title={strings.forgotPassword()}
style={{
alignSelf: "flex-end",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
paddingHorizontal: 0
}}
onPress={() => {
if (loading) return;
SheetManager.show("forgotpassword_sheet", email.current);
}}
textStyle={{
textDecorationLine: "underline"
}}
fontSize={AppFontSize.xs}
type="plain"
/>
</>
)}
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
/>
</View>
<Heading
style={{
marginBottom: 25,
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
extraBold
size={AppFontSize.xxl}
>
{strings.loginToYourAccount()}
</Heading>
</View>
<View>
<View
style={{
width: DDS.isTab
? focused
? "50%"
: "49.99%"
: focused
? "100%"
: "99.9%",
backgroundColor: colors.primary.background,
alignSelf: "center",
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<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",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
paddingHorizontal: 0
}}
onPress={() => {
if (loading) return;
login();
SheetManager.show("forgotpassword_sheet", email.current);
}}
style={{
width: "100%"
textStyle={{
textDecorationLine: "underline"
}}
type="accent"
title={!loading ? strings.continue() : null}
fontSize={AppFontSize.sm}
fontSize={AppFontSize.xs}
type="plain"
/>
</>
)}
{step === LoginSteps.passwordAuth && (
<Button
title={strings.cancelLogin()}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
}}
onPress={() => {
if (loading) return;
setStep(LoginSteps.emailAuth);
setLoading(false);
}}
type="secondaryAccented"
/>
)}
<View
style={{
marginTop: 25
}}
>
<Button
loading={loading}
onPress={() => {
if (loading) return;
login();
}}
style={{
width: 250
}}
type="accent"
title={!loading ? strings.continue() : null}
/>
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
{step === LoginSteps.passwordAuth && (
<Button
title={strings.cancelLogin()}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
width: 250
}}
onPress={() => {
if (loading) return;
setStep(LoginSteps.emailAuth);
setLoading(false);
}}
fontSize={AppFontSize.xs}
type="secondaryAccented"
/>
)}
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
{strings.dontHaveAccount()}{" "}
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{ color: colors.primary.accent }}
>
{strings.dontHaveAccount()}{" "}
<Paragraph
size={AppFontSize.xs}
style={{ color: colors.primary.accent }}
>
{strings.signUp()}
</Paragraph>
{strings.signUp()}
</Paragraph>
</TouchableOpacity>
) : null}
</View>
</Paragraph>
</TouchableOpacity>
) : null}
</View>
</View>
</KeyboardAwareScrollView>
</View>
</>
);
};

View File

@@ -1,27 +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, { useContext } from "react";
export const SignupContext = React.createContext<{
signup?: () => Promise<boolean>;
}>({
signup: undefined
});
export const useSignupContext = () => useContext(SignupContext);

View File

@@ -19,36 +19,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { useRoute } from "@react-navigation/native";
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 Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
import { useUserStore } from "../../stores/use-user-store";
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Loading } from "../loading";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { AuthHeader } from "./header";
import { SignupContext } from "./signup-context";
import SettingsService from "../../services/settings";
import { hideAuth } from "./common";
import { DefaultAppStyles } from "../../utils/styles";
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();
@@ -62,8 +52,6 @@ export const Signup = ({ changeMode, welcome }) => {
const setLastSynced = useUserStore((state) => state.setLastSynced);
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
const route = useRoute();
const validateInfo = () => {
if (!password.current || !email.current || !confirmPassword.current) {
ToastManager.show({
@@ -82,26 +70,23 @@ 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();
if (!SettingsService.getProperty("serverUrls")) {
Navigation.navigate("PayWall", {
canGoBack: false,
state: route.params.state,
context: "signup"
});
hideAuth();
SettingsService.setProperty("encryptedBackup", true);
await sleep(300);
if (trial) {
PremiumService.sheet(null, null, true);
} else {
PremiumService.showVerifyEmailDialog();
}
return true;
} catch (e) {
setCurrentStep(SignupSteps.signup);
setLoading(false);
ToastManager.show({
heading: strings.signupFailed(),
@@ -109,264 +94,214 @@ export const Signup = ({ changeMode, welcome }) => {
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%",
height: "100%",
alignSelf: "center"
}}
>
<View
style={{
justifyContent: "flex-end",
paddingHorizontal: DefaultAppStyles.GAP,
backgroundColor: colors.secondary.background,
borderBottomWidth: 0.8,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderBottomColor: colors.primary.border,
alignSelf: isTablet ? "center" : undefined,
borderWidth: isTablet ? 1 : null,
borderColor: isTablet ? colors.primary.border : null,
borderRadius: isTablet ? 20 : null,
marginTop: isTablet ? 50 : null,
width: !isTablet ? null : "70%",
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={{
justifyContent: "flex-end",
paddingHorizontal: 16,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderBottomWidth: 0.8,
borderBottomColor: colors.primary.border,
alignSelf: isTablet ? "center" : undefined,
borderWidth: isTablet ? 1 : null,
borderColor: isTablet ? colors.primary.border : null,
borderRadius: isTablet ? 20 : null,
marginTop: isTablet ? 50 : null,
width: !isTablet ? null : "50%",
minHeight: height * 0.25
}}
>
<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
extraBold
style={{
marginBottom: 25,
marginTop: 10
}}
size={AppFontSize.xxl}
>
{strings.createAccount()}
</Heading>
</View>
<View
style={{
width: 20,
height: 5,
backgroundColor: colors.secondary.background,
borderRadius: 2
}}
/>
</View>
<Heading
extraBold
style={{
marginBottom: 25,
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
size={AppFontSize.xxl}
>
{strings.createYourAccount()}
</Heading>
</View>
<View
style={{
width: DDS.isTab ? "50%" : "100%",
paddingHorizontal: DDS.isTab ? 0 : 16,
backgroundColor: colors.primary.background,
flexGrow: 1,
alignSelf: "center"
}}
>
<Input
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
defaultValue={email.current}
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()}
blurOnSubmit={false}
onSubmit={() => {
if (!email.current) return;
passwordInputRef.current?.focus();
}}
/>
<Input
fwdRef={passwordInputRef}
onChangeText={(value) => {
password.current = value;
}}
defaultValue={password.current}
testID="input.password"
onErrorCheck={(e) => setError(e)}
returnKeyLabel="Next"
returnKeyType="next"
secureTextEntry
autoComplete="password"
autoCapitalize="none"
blurOnSubmit={false}
validationType="password"
autoCorrect={false}
placeholder={strings.password()}
onSubmit={() => {
if (!password.current) return;
confirmPasswordInputRef.current?.focus();
}}
/>
<Input
fwdRef={confirmPasswordInputRef}
onChangeText={(value) => {
confirmPassword.current = value;
}}
defaultValue={confirmPassword.current}
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={strings.confirmPassword()}
marginBottom={12}
onSubmit={signup}
/>
<Button
title={!loading ? "Continue" : null}
type="accent"
loading={loading}
onPress={() => {
signup();
}}
width="100%"
/>
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(0);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: 12,
paddingVertical: 12
}}
>
<Paragraph
size={AppFontSize.xs + 1}
color={colors.secondary.paragraph}
>
{strings.alreadyHaveAccount()}{" "}
<Paragraph
size={AppFontSize.xs + 1}
style={{ color: colors.primary.accent }}
>
{strings.login()}
</Paragraph>
</Paragraph>
</TouchableOpacity>
</View>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Paragraph
style={{
marginBottom: 25,
textAlign: "center"
}}
size={AppFontSize.xxs}
color={colors.secondary.paragraph}
>
{strings.signupAgreement[0]()}
<Paragraph
size={AppFontSize.xxs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos", colors);
}}
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
{" "}
{strings.signupAgreement[1]()}
</Paragraph>{" "}
{strings.signupAgreement[2]()}
<Paragraph
size={AppFontSize.xxs}
onPress={() => {
openLinkInBrowser(
"https://notesnook.com/privacy",
colors
);
}}
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
{" "}
{strings.signupAgreement[3]()}
</Paragraph>{" "}
{strings.signupAgreement[4]()}
</Paragraph>
</View>
</View>
</KeyboardAwareScrollView>
</>
) : (
<>
<Loading
title={"Setting up your account..."}
description="Your account is almost ready, please wait..."
<View
style={{
width: DDS.isTab ? "50%" : "100%",
paddingHorizontal: DefaultAppStyles.GAP,
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();
}}
/>
</>
)}
</SignupContext.Provider>
<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={AppFontSize.xxs}
color={colors.secondary.paragraph}
>
{strings.signupAgreement[0]()}
<Paragraph
size={AppFontSize.xxs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos", colors);
}}
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
{" "}
{strings.signupAgreement[1]()}
</Paragraph>{" "}
{strings.signupAgreement[2]()}
<Paragraph
size={AppFontSize.xxs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy", colors);
}}
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
{" "}
{strings.signupAgreement[3]()}
</Paragraph>{" "}
{strings.signupAgreement[4]()}
</Paragraph>
<Button
title={!loading ? strings.continue() : null}
type="accent"
loading={loading}
onPress={signup}
style={{
width: 250
}}
/>
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(0);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph size={AppFontSize.xs} color={colors.secondary.paragraph}>
{strings.alreadyHaveAccount()}{" "}
<Paragraph
size={AppFontSize.xs}
style={{ color: colors.primary.accent }}
>
{strings.login()}
</Paragraph>
</Paragraph>
</TouchableOpacity>
</View>
</View>
</>
);
};

View File

@@ -24,8 +24,12 @@ 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 { eSendEvent, ToastManager } from "../../services/event-manager";
import { eCloseSimpleDialog } from "../../utils/events";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../services/event-manager";
import { eCloseSheet } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
@@ -34,7 +38,6 @@ import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
import { presentDialog } from "../dialog/functions";
const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
const { colors } = useThemeColors();
@@ -43,7 +46,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
method: mfaInfo?.primaryMethod,
isPrimary: true
});
const { seconds, start, reset } = useTimer(currentMethod.method);
const { seconds, start } = useTimer(currentMethod.method);
const [loading, setLoading] = useState(false);
const inputRef = useRef();
const [sending, setSending] = useState(false);
@@ -59,7 +62,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
},
(result) => {
if (result) {
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
eSendEvent(eCloseSheet, "two_factor_verify");
}
setLoading(false);
}
@@ -130,17 +133,6 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
<ScrollView
keyboardShouldPersistTaps="handled"
keyboardDismissMode="interactive"
style={{
width: "100%",
height: "100%",
backgroundColor: colors.primary.background,
paddingTop: 60
}}
onLayout={() => {
setTimeout(() => {
inputRef.current?.focus();
}, 500);
}}
>
<View
style={{
@@ -211,15 +203,13 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
code.current = value;
//onNext();
}}
cursorColor={colors.selected.accent}
selectionHandleColor={colors.selected.accent}
selectionColor={colors.selected.accent}
onSubmitEditing={onNext}
caretHidden
height={60}
inputStyle={{
fontSize: AppFontSize.lg,
textAlign: "center",
letterSpacing: 7,
letterSpacing: 10,
width: 250
}}
keyboardType={
@@ -227,6 +217,8 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
}
enablesReturnKeyAutomatically
containerStyle={{
borderWidth: 0,
width: undefined,
minWidth: "50%"
}}
wrapperStyle={{
@@ -244,10 +236,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
<Button
title={strings.cancel()}
type="secondaryAccented"
onPress={() => {
reset();
onCancel();
}}
onPress={onCancel}
width={250}
/>
@@ -308,7 +297,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
};
TwoFactorVerification.present = (onMfaLogin, data, onCancel, context) => {
presentDialog({
presentSheet({
component: () => (
<TwoFactorVerification
onMfaLogin={onMfaLogin}
@@ -317,9 +306,7 @@ TwoFactorVerification.present = (onMfaLogin, data, onCancel, context) => {
/>
),
context: context || "two_factor_verify",
disableClosing: true,
transparent: false,
statusBarTranslucent: true
disableClosing: true
});
};

View File

@@ -17,17 +17,16 @@ 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 { 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 { eCloseSimpleDialog } from "../../utils/events";
import { eCloseSheet } from "../../utils/events";
import TwoFactorVerification from "./two-factor";
import { strings } from "@notesnook/intl";
export const LoginSteps = {
emailAuth: 1,
@@ -35,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 (
@@ -73,15 +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);
if (mfaInfo) {
TwoFactorVerification.present(
async (mfa: any, callback: (success: boolean) => void) => {
async (mfa, callback) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
@@ -99,8 +91,8 @@ export const useLogin = (
callback && callback(false);
} catch (e) {
callback && callback(false);
if ((e as Error).message === "invalid_grant") {
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
if (e.message === "invalid_grant") {
eSendEvent(eCloseSheet, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
}
@@ -108,7 +100,7 @@ export const useLogin = (
},
mfaInfo,
() => {
eSendEvent(eCloseSimpleDialog, "two_factor_verify");
eSendEvent(eCloseSheet, "two_factor_verify");
setLoading(false);
setStep(LoginSteps.emailAuth);
}
@@ -119,14 +111,10 @@ export const useLogin = (
break;
}
case LoginSteps.passwordAuth: {
if (!email.current || !password.current) {
setLoading(false);
return;
}
await db.user.authenticatePassword(
email.current,
password.current,
undefined,
null,
sessionExpired
);
finishLogin();
@@ -135,11 +123,11 @@ 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({
@@ -158,7 +146,7 @@ export const useLogin = (
clearMessage();
ToastManager.show({
heading: strings.loginSuccess(),
message: strings.loginSuccessDesc(user.email),
message: strings.loginSuccessDesc(),
type: "success",
context: "global"
});

View File

@@ -36,7 +36,6 @@ import { getElevationStyle } from "../../utils/elevation";
import { AppFontSize, normalize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { hexToRGBA, RGB_Linear_Shade } from "../../utils/colors";
import useKeyboard from "../../hooks/use-keyboard";
interface FloatingButtonProps {
onPress: () => void;

View File

@@ -31,39 +31,37 @@ import ResultDialog from "../dialogs/result";
import { VaultDialog } from "../dialogs/vault";
import ImagePreview from "../image-preview";
import MergeConflicts from "../merge-conflicts";
import PremiumDialog from "../premium";
import { Expiring } from "../premium/expiring";
import SheetProvider from "../sheet-provider";
import RateAppSheet from "../sheets/rate-app";
import RecoveryKeySheet from "../sheets/recovery-key";
import Progress from "../dialogs/progress";
import { useSettingStore } from "../../stores/use-setting-store";
const DialogProvider = () => {
const { colors } = useThemeColors();
const isAppLoading = useSettingStore((state) => state.isAppLoading);
return (
<>
<AppLockPassword />
<LoadingDialog />
<Dialog context="global" />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
<RecoveryKeySheet colors={colors} />
<SheetProvider />
<SheetProvider context="sync_progress" />
<Dialog context="global" />
<ResultDialog />
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />
<Expiring />
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
<Progress />
{isAppLoading ? null : (
<>
<MergeConflicts />
<RecoveryKeySheet colors={colors} />
<ResultDialog />
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />
<JumpToSectionDialog />
</>
)}
</>
);
};

View File

@@ -26,7 +26,7 @@ import { notesnook } from "../../../e2e/test.ids";
import { getColorLinearShade } from "../../utils/colors";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { Button, ButtonProps } from "../ui/button";
import { Button } from "../ui/button";
import Paragraph from "../ui/typography/paragraph";
const DialogButtons = ({
@@ -37,14 +37,6 @@ const DialogButtons = ({
loading,
doneText,
positiveType
}: {
onPressPositive?: () => void;
onPressNegative: () => void;
positiveTitle: string;
negativeTitle?: string;
loading?: boolean;
doneText?: string;
positiveType?: ButtonProps["type"];
}) => {
const { colors, isDark } = useThemeColors();

View File

@@ -21,7 +21,7 @@ import React from "react";
import { Text, View, ViewStyle } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import { Button, ButtonProps } from "../ui/button";
import { Button } from "../ui/button";
import { PressableProps } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
@@ -31,7 +31,13 @@ type DialogHeaderProps = {
icon?: string;
title?: string;
paragraph?: string;
button?: ButtonProps;
button?: {
onPress?: () => void;
loading?: boolean;
title?: string;
type?: PressableProps["type"];
icon?: string;
};
paragraphColor?: string;
padding?: number;
centered?: boolean;
@@ -89,14 +95,17 @@ const DialogHeader = ({
{button ? (
<Button
onPress={button.onPress}
style={{
borderRadius: 100,
paddingHorizontal: DefaultAppStyles.GAP
}}
loading={button.loading}
fontSize={13}
title={button.title}
icon={button.icon}
type={button.type || "secondary"}
height={30}
{...button}
/>
) : null}
</View>

View File

@@ -17,20 +17,15 @@ 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 { KeyboardTypeOptions } from "react-native";
import { eSendEvent } from "../../services/event-manager";
import { eCloseSimpleDialog, eOpenSimpleDialog } from "../../utils/events";
import { ButtonProps } from "../ui/button";
export type DialogInfo = {
type DialogInfo = {
title?: string;
paragraph?: string;
positiveText: string;
negativeText: string;
background?: string;
transparent?: boolean;
statusBarTranslucent?: boolean;
positivePress?: (...args: any[]) => Promise<any>;
positiveText?: string;
negativeText?: string;
positivePress?: (...args: any[]) => void;
onClose?: () => void;
positiveType?:
| "transparent"
@@ -49,18 +44,16 @@ export type DialogInfo = {
// eslint-disable-next-line @typescript-eslint/ban-types
context: "global" | "local" | (string & {});
secureTextEntry?: boolean;
keyboardType?: KeyboardTypeOptions;
keyboardType?: string;
check?: {
info: string;
type?: ButtonProps["type"];
type?: string;
defaultValue?: boolean;
};
notice: {
text: string;
type: "alert" | "information";
};
disableBackdropClosing: boolean;
component: JSX.Element | ((close?: () => void) => JSX.Element);
};
export function presentDialog(data: Partial<DialogInfo>): void {

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { TextInput, View, ViewStyle } from "react-native";
import { View } from "react-native";
import { DDS } from "../../services/device-detection";
import {
eSubscribeEvent,
@@ -40,26 +40,42 @@ import Seperator from "../ui/seperator";
import BaseDialog from "./base-dialog";
import DialogButtons from "./dialog-buttons";
import DialogHeader from "./dialog-header";
import { DialogInfo } from "./functions";
export const Dialog = ({ context = "global" }: { context?: string }) => {
export const Dialog = ({ context = "global" }) => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState<boolean>();
const [checked, setChecked] = useState<boolean>();
const [loading, setLoading] = useState<boolean>();
const values = useRef<{
inputValue?: string;
}>({
const [visible, setVisible] = useState(false);
const [checked, setChecked] = useState(false);
const [loading, setLoading] = useState(false);
const values = useRef({
inputValue: undefined
});
const inputRef = useRef<TextInput>(null);
const [dialogInfo, setDialogInfo] = useState<DialogInfo>();
const inputRef = useRef();
const [dialogInfo, setDialogInfo] = useState({
title: "",
paragraph: "",
positiveText: strings.done(),
negativeText: strings.cancel(),
positivePress: () => {},
onClose: () => {},
positiveType: "transparent",
icon: null,
paragraphColor: colors.primary.paragraph,
input: false,
inputPlaceholder: "Enter some text",
defaultValue: "",
disableBackdropClosing: false,
check: {
info: "Check",
type: "transparent",
defaultValue: false
}
});
const onPressPositive = async () => {
if (dialogInfo?.positivePress) {
if (dialogInfo.positivePress) {
inputRef.current?.blur();
setLoading(true);
let result = false;
let result;
try {
result = await dialogInfo.positivePress(
values.current.inputValue || dialogInfo.defaultValue,
@@ -81,7 +97,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
};
const show = useCallback(
(data: DialogInfo) => {
(data) => {
if (!data.context) data.context = "global";
if (data.context !== context) return;
setDialogInfo(data);
@@ -92,14 +108,6 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
[context]
);
const hide = React.useCallback(() => {
setChecked(false);
values.current.inputValue = undefined;
setVisible(false);
setDialogInfo(undefined);
dialogInfo?.onClose?.();
}, [dialogInfo]);
useEffect(() => {
eSubscribeEvent(eOpenSimpleDialog, show);
eSubscribeEvent(eCloseSimpleDialog, hide);
@@ -110,14 +118,21 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
};
}, [hide, show]);
const hide = React.useCallback(() => {
setChecked(false);
values.current.inputValue = undefined;
setVisible(false);
dialogInfo.onClose?.();
}, [dialogInfo]);
const onNegativePress = async () => {
if (dialogInfo?.onClose) {
if (dialogInfo.onClose) {
await dialogInfo.onClose();
}
hide();
};
const style: ViewStyle = {
const style = {
...getElevationStyle(5),
width: DDS.isTab ? 400 : "85%",
maxHeight: 450,
@@ -128,19 +143,11 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
overflow: "hidden"
};
return visible && dialogInfo ? (
return visible ? (
<BaseDialog
statusBarTranslucent={
dialogInfo.statusBarTranslucent === undefined
? false
: dialogInfo.statusBarTranslucent
}
statusBarTranslucent={false}
bounce={!dialogInfo.input}
closeOnTouch={!dialogInfo.disableBackdropClosing}
background={dialogInfo.background}
transparent={
dialogInfo.transparent === undefined ? true : dialogInfo.transparent
}
onShow={async () => {
if (dialogInfo.input) {
inputRef.current?.setNativeProps({
@@ -153,99 +160,89 @@ export const Dialog = ({ context = "global" }: { context?: string }) => {
visible={true}
onRequestClose={hide}
>
{typeof dialogInfo.component === "function"
? dialogInfo.component(() => hide())
: dialogInfo.component}
<View style={style}>
<DialogHeader
title={dialogInfo.title}
icon={dialogInfo.icon}
paragraph={dialogInfo.paragraph}
paragraphColor={dialogInfo.paragraphColor}
padding={12}
style={{
minHeight: 0
}}
/>
<Seperator half />
{dialogInfo.component ? null : (
<View style={style}>
<DialogHeader
title={dialogInfo.title}
icon={dialogInfo.icon}
paragraph={dialogInfo.paragraph}
paragraphColor={dialogInfo.paragraphColor}
padding={12}
{dialogInfo.input ? (
<View
style={{
minHeight: 0
paddingHorizontal: DefaultAppStyles.GAP
}}
/>
<Seperator half />
{dialogInfo.input ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
>
<Input
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.inputValue = value;
}}
>
<Input
fwdRef={inputRef}
autoCapitalize="none"
onChangeText={(value) => {
values.current.inputValue = value;
}}
testID="input-value"
secureTextEntry={dialogInfo.secureTextEntry}
//defaultValue={dialogInfo.defaultValue}
onSubmit={() => {
onPressPositive();
}}
returnKeyLabel="Done"
returnKeyType="done"
keyboardType={dialogInfo.keyboardType || "default"}
placeholder={dialogInfo.inputPlaceholder}
/>
</View>
) : null}
testID="input-value"
secureTextEntry={dialogInfo.secureTextEntry}
//defaultValue={dialogInfo.defaultValue}
onSubmit={onPressPositive}
returnKeyLabel="Done"
returnKeyType="done"
keyboardType={dialogInfo.keyboardType || "default"}
placeholder={dialogInfo.inputPlaceholder}
/>
</View>
) : null}
{dialogInfo?.notice ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
{dialogInfo?.notice ? (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Notice
type={dialogInfo.notice.type || "information"}
text={dialogInfo.notice.text}
/>
</View>
) : null}
{dialogInfo.check ? (
<>
<Button
onPress={() => {
setChecked(!checked);
}}
>
<Notice
type={dialogInfo.notice.type || "information"}
text={dialogInfo.notice.text}
/>
</View>
) : null}
icon={
checked
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
iconColor={checked ? colors.secondary.icon : colors.primary.icon}
style={{
justifyContent: "flex-start"
}}
height={35}
iconSize={20}
width="100%"
title={dialogInfo.check.info}
type={checked ? dialogInfo.check.type || "plain" : "plain"}
/>
</>
) : null}
{dialogInfo.check ? (
<>
<Button
onPress={() => {
setChecked(!checked);
}}
icon={
checked
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
iconColor={
checked ? colors.secondary.icon : colors.primary.icon
}
style={{
justifyContent: "flex-start"
}}
height={35}
iconSize={20}
width="100%"
title={dialogInfo.check.info}
type={checked ? dialogInfo.check.type || "plain" : "plain"}
/>
</>
) : null}
<DialogButtons
onPressNegative={onNegativePress}
onPressPositive={dialogInfo.positivePress && onPressPositive}
loading={loading}
positiveTitle={dialogInfo.positiveText}
negativeTitle={dialogInfo.negativeText}
positiveType={dialogInfo.positiveType}
/>
</View>
)}
<DialogButtons
onPressNegative={onNegativePress}
onPressPositive={dialogInfo.positivePress && onPressPositive}
loading={loading}
positiveTitle={dialogInfo.positiveText}
negativeTitle={dialogInfo.negativeText}
positiveType={dialogInfo.positiveType}
/>
</View>
<Toast context="local" />
</BaseDialog>
) : null;

View File

@@ -16,9 +16,6 @@ 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 { useIsFeatureAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useState } from "react";
import {
Image,
@@ -28,13 +25,15 @@ import {
View
} from "react-native";
import { Image as ImageType } from "react-native-image-crop-picker";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { useThemeColors } from "@notesnook/theme";
import { presentSheet } from "../../../services/event-manager";
import { defaultBorderRadius, AppFontSize } 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";
import { DefaultAppStyles } from "../../../utils/styles";
export default function AttachImage({
response,
@@ -45,7 +44,6 @@ export default function AttachImage({
onAttach: ({ compress }: { compress: boolean }) => void;
close: ((ctx?: string | undefined) => void) | undefined;
}) {
const fullQualityImagesResult = useIsFeatureAvailable("fullQualityImages");
const { colors } = useThemeColors();
const [compress, setCompress] = useState(true);
@@ -106,42 +104,21 @@ export default function AttachImage({
alignSelf: "center",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
alignItems: "center",
width: "100%",
opacity: fullQualityImagesResult?.isAllowed ? 1 : 0.5
width: "100%"
}}
onPress={() => {
if (!fullQualityImagesResult?.isAllowed) {
ToastManager.show({
message: fullQualityImagesResult?.error,
type: "info",
context: "local"
});
return;
}
setCompress(!compress);
}}
>
<IconButton
size={AppFontSize.lg}
name={compress ? "checkbox-marked" : "checkbox-blank-outline"}
color={
compress && fullQualityImagesResult?.isAllowed
? colors.primary.accent
: colors.primary.icon
}
color={compress ? colors.primary.accent : colors.primary.icon}
style={{
width: 25,
height: 25
}}
onPress={() => {
if (!fullQualityImagesResult?.isAllowed) {
ToastManager.show({
message: fullQualityImagesResult?.error,
type: "info",
context: "local"
});
return;
}
setCompress(!compress);
}}
/>

View File

@@ -17,15 +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, { useCallback, useEffect, useRef, useState } from "react";
import { Dimensions, TextInput, View } from "react-native";
import Orientation from "react-native-orientation-locker";
import Pdf from "react-native-pdf";
import { MMKV } from "../../../common/database/mmkv";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import downloadAttachment from "../../../common/filesystem/download-attachment";
import { deleteCacheFileByPath, exists } from "../../../common/filesystem/io";
import { cacheDir } from "../../../common/filesystem/utils";
import { useAttachmentProgress } from "../../../hooks/use-attachment-progress";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
@@ -33,9 +30,8 @@ import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { sleep } from "../../../utils/time";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import { presentDialog } from "../../dialog/functions";
@@ -43,6 +39,11 @@ import SheetProvider from "../../sheet-provider";
import { IconButton } from "../../ui/icon-button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
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";
import { DefaultAppStyles } from "../../../utils/styles";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
@@ -104,19 +105,15 @@ const PDFPreview = () => {
const open = useCallback(
async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.hash;
if (!hash) return;
if (!(await exists(hash))) setLoading(true);
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
if (!(await exists(hash))) {
setVisible(false);
return;
}
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
@@ -169,7 +166,8 @@ const PDFPreview = () => {
}}
>
{loading ? (
<View
<Animated.View
exiting={FadeOut}
style={{
flex: 1,
justifyContent: "center",
@@ -190,7 +188,7 @@ const PDFPreview = () => {
>
{strings.loadingWithProgress(progress?.percent)}
</Paragraph>
</View>
</Animated.View>
) : (
<>
<View
@@ -244,7 +242,7 @@ const PDFPreview = () => {
textAlign: "center",
marginRight: 4,
borderRadius: 3,
fontFamily: "Inter-Regular"
fontFamily: "OpenSans-Regular"
}}
selectTextOnFocus
keyboardType="decimal-pad"
@@ -272,42 +270,49 @@ const PDFPreview = () => {
</View>
</View>
{pdfSource ? (
<Pdf
source={{
uri: pdfSource
}}
ref={pdfRef}
onLoadComplete={(numberOfPages) => {
setNumPages(numberOfPages);
}}
onPageChanged={(page) => {
setCurrentPage(page);
inputRef.current?.setNativeProps({
text: page + ""
});
saveSnapshot({
currentPage: page,
scale: snapshot?.current?.scale
});
}}
// scale={snapshotValue.current?.scale}
// onScaleChanged={(scale) => {
// saveSnapshot({
// currentPage: snapshot?.current?.currentPage,
// scale: scale
// });
// }}
page={snapshotValue?.current?.currentPage}
password={password}
maxScale={6}
onError={onError}
onPressLink={(uri) => {}}
<Animated.View
style={{
flex: 1,
width: width,
height: Dimensions.get("window").height
flex: 1
}}
/>
entering={FadeIn}
>
<Pdf
source={{
uri: pdfSource
}}
ref={pdfRef}
onLoadComplete={(numberOfPages) => {
setNumPages(numberOfPages);
}}
onPageChanged={(page) => {
setCurrentPage(page);
inputRef.current?.setNativeProps({
text: page + ""
});
saveSnapshot({
currentPage: page,
scale: snapshot?.current?.scale
});
}}
// scale={snapshotValue.current?.scale}
// onScaleChanged={(scale) => {
// saveSnapshot({
// currentPage: snapshot?.current?.currentPage,
// scale: scale
// });
// }}
page={snapshotValue?.current?.currentPage}
password={password}
maxScale={6}
onError={onError}
onPressLink={(uri) => {}}
style={{
flex: 1,
width: width,
height: Dimensions.get("window").height
}}
/>
</Animated.View>
) : null}
</>
)}

View File

@@ -135,6 +135,7 @@ const ResultDialog = () => {
paddingHorizontal: DefaultAppStyles.GAP
}}
onPress={close}
fontSize={AppFontSize.md + 2}
/>
</View>
</View>

View File

@@ -30,7 +30,6 @@ import { BackHandler, Platform, ViewProps } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
runOnJS,
SharedValue,
useAnimatedReaction,
useAnimatedStyle,
useSharedValue,
@@ -68,7 +67,6 @@ export interface TabsRef {
setScrollEnabled: () => true;
isDrawerOpen: () => boolean;
node: RefObject<Animated.View>;
tabChangedFromSwipeAction: SharedValue<boolean>;
}
export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
@@ -118,7 +116,6 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
const isLoaded = useRef(false);
const prevWidths = useRef(widths);
const isIPhone = Platform.OS === "ios";
const tabChangedFromSwipeAction = useSharedValue(false);
useEffect(() => {
if (deviceMode === "tablet" || fullscreen) {
@@ -194,7 +191,6 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
: withTiming(editorPosition);
currentTab.value = 2;
}
tabChangedFromSwipeAction.value = false;
},
goToIndex: (index: number, animated = true) => {
if (deviceMode === "tablet") {
@@ -214,7 +210,6 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
: editorPosition;
currentTab.value = 2;
}
tabChangedFromSwipeAction.value = false;
},
unlock: () => {
forcedLock.value = false;
@@ -257,8 +252,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
},
page: () => (currentTab.value === 1 ? "home" : "editor"),
setScrollEnabled: () => true,
node: node,
tabChangedFromSwipeAction: tabChangedFromSwipeAction
node: node
}),
[
currentTab,
@@ -268,8 +262,7 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
homePosition,
editorPosition,
forcedLock,
isDrawerOpen,
tabChangedFromSwipeAction
isDrawerOpen
]
);
@@ -369,20 +362,18 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
isDrawerOpen.value = true;
currentTab.value = 1;
runOnJS(onDrawerStateChange)(true);
tabChangedFromSwipeAction.value = true;
return;
} else if (!isSwipeLeft && finalValue > 100) {
translateX.value = withSpring(homePosition, animationConfig);
isDrawerOpen.value = false;
currentTab.value = 1;
runOnJS(onDrawerStateChange)(false);
tabChangedFromSwipeAction.value = true;
return;
} else if (!isSwipeLeft && finalValue < 100) {
translateX.value = withSpring(0, animationConfig);
isDrawerOpen.value = true;
currentTab.value = 1;
tabChangedFromSwipeAction.value = true;
return;
}
}
@@ -393,8 +384,6 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
translateX.value = withSpring(editorPosition, animationConfig);
currentTab.value = 2;
isDrawerOpen.value = false;
tabChangedFromSwipeAction.value = true;
return;
}
}
@@ -409,8 +398,6 @@ export const FluidPanels = forwardRef<TabsRef, TabProps>(function FluidTabs(
translateX.value = withSpring(editorPosition, animationConfig);
currentTab.value = 2;
}
tabChangedFromSwipeAction.value = true;
return;
}

View File

@@ -97,6 +97,7 @@ export const Header = ({
style={{
flexDirection: "row",
justifyContent: "space-between",
marginTop: DefaultAppStyles.GAP_SMALL,
borderRadius: 10,
paddingVertical: 3,
borderWidth: hasSearch ? 1 : 0,

View File

@@ -20,22 +20,21 @@ 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, useWindowDimensions, View } from "react-native";
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 Navigation from "../../services/navigation";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { AuthMode } from "../auth/common";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import SettingsService from "../../services/settings";
import { SafeAreaView } from "react-native-safe-area-context";
import { AuthMode } from "../auth/common";
import { DefaultAppStyles } from "../../utils/styles";
const Intro = () => {
const { colors } = useThemeColors();
const { width } = useWindowDimensions();
const { width, height } = useWindowDimensions();
const insets = useGlobalSafeAreaInsets();
const isTablet = width > 600;
@@ -116,102 +115,75 @@ const Intro = () => {
);
return (
<SafeAreaView
<ScrollView
testID="notesnook.splashscreen"
style={{
flex: 1,
height: "100%",
width: "100%",
backgroundColor: colors.primary.background
}}
>
<View
testID="notesnook.splashscreen"
style={{
flex: 1
}}
style={[
{
width: "100%",
backgroundColor: colors.secondary.background,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
paddingTop: insets.top + 10,
paddingBottom: insets.top + 10,
minHeight: height * 0.7 - (insets.top + insets.bottom)
},
isTablet && {
width: width / 2,
alignSelf: "center",
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: 20,
marginTop: 50
}
]}
>
<View
style={[
{
width: "100%",
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
paddingTop: insets.top + 10,
paddingBottom: insets.top + 10,
flexGrow: 1
},
isTablet && {
width: width / 2,
alignSelf: "center",
borderWidth: 1,
borderColor: colors.primary.border,
borderRadius: 20,
marginTop: 50
}
]}
>
<SwiperFlatList
autoplay
autoplayDelay={10}
autoplayLoop={true}
index={0}
useReactNativeGestureHandler={true}
showPagination
data={strings.introData}
paginationActiveColor={colors.primary.accent}
paginationStyleItem={{
width: 10,
height: 5,
marginRight: 4,
marginLeft: 4
}}
paginationDefaultColor={colors.primary.border}
renderItem={renderItem}
/>
</View>
<SwiperFlatList
autoplay
autoplayDelay={10}
autoplayLoop={true}
index={0}
useReactNativeGestureHandler={true}
showPagination
data={strings.introData}
paginationActiveColor={colors.primary.accent}
paginationStyleItem={{
width: 10,
height: 5,
marginRight: 4,
marginLeft: 4
}}
paginationDefaultColor={colors.primary.border}
renderItem={renderItem}
/>
</View>
<View
style={{
width: isTablet ? "50%" : "100%",
width: "100%",
justifyContent: "center",
gap: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: isTablet ? 0 : DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
flexShrink: 1,
alignSelf: "center"
minHeight: height * 0.3
}}
>
<Button
style={{
width: "100%"
}}
width={250}
onPress={async () => {
SettingsService.set({ introCompleted: true });
Navigation.push("Auth", {
mode: AuthMode.welcomeSignup
});
}}
fontSize={AppFontSize.md}
type="accent"
title={strings.getStarted()}
/>
<Button
style={{
width: "100%"
}}
title={strings.iAlreadyHaveAnAccount()}
type="secondary"
onPress={() => {
SettingsService.set({
introCompleted: true
});
Navigation.push("Auth", {
mode: AuthMode.welcomeLogin,
context: "intro"
});
}}
/>
</View>
</SafeAreaView>
</ScrollView>
);
};

View File

@@ -44,8 +44,9 @@ export const Header = React.memo(
}: ListHeaderProps) => {
const { colors } = useThemeColors();
const announcements = useMessageStore((state) => state.announcements);
const selectionMode = useSelectionStore((state) => state.selectionMode);
return (
return selectionMode ? null : (
<>
{announcements.length !== 0 && !noAnnouncement ? (
<Announcement color={color || colors.primary.accent} />

View File

@@ -133,9 +133,7 @@ export const SectionHeader = React.memo<
<Sort
screen={screen}
type={dataType}
hideGroupOptions={
screen === "Reminders" || screen === "Search"
}
hideGroupOptions={screen === "Reminders"}
/>
)
});

View File

@@ -41,7 +41,7 @@ export const openNotebook = (item: Notebook | BaseTrashItem<Notebook>) => {
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({

View File

@@ -17,26 +17,24 @@ 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 { Reminder } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../../e2e/test.ids";
import useIsSelected from "../../../hooks/use-selected";
import AddReminder from "../../../screens/add-reminder";
import { eSendEvent } from "../../../services/event-manager";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { eCloseSheet } from "../../../utils/events";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { Properties } from "../../properties";
import AppIcon from "../../ui/AppIcon";
import ReminderSheet from "../../sheets/reminder";
import { IconButton } from "../../ui/icon-button";
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";
import { useSelectionStore } from "../../../stores/use-selection-store";
import useIsSelected from "../../../hooks/use-selected";
import AppIcon from "../../ui/AppIcon";
import { DefaultAppStyles } from "../../../utils/styles";
const ReminderItem = React.memo(
({
@@ -51,10 +49,8 @@ const ReminderItem = React.memo(
const { colors } = useThemeColors();
const openReminder = () => {
if (selectItem(item)) return;
AddReminder.present(item, undefined);
if (isSheet) {
eSendEvent(eCloseSheet);
}
ReminderSheet.present(item, undefined, isSheet);
};
const selectionMode = useSelectionStore((state) => state.selectionMode);
const [selected] = useIsSelected(item);

View File

@@ -1,180 +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 { HighlightedResult } from "@notesnook/core";
import { View } from "react-native";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { useThemeColors } from "@notesnook/theme";
import { DefaultAppStyles } from "../../../utils/styles";
import Heading from "../../ui/typography/heading";
import { AppFontSize } from "../../../utils/size";
import { Properties } from "../../properties";
import { db } from "../../../common/database";
import { eSendEvent } from "../../../services/event-manager";
import { eOnLoadNote } from "../../../utils/events";
import { IconButton } from "../../ui/icon-button";
import { fluidTabsRef } from "../../../utils/global-refs";
type SearchResultProps = {
item: HighlightedResult;
};
export const SearchResult = (props: SearchResultProps) => {
const [expanded, setExpanded] = React.useState(true);
const { colors } = useThemeColors();
const openNote = async (index?: number) => {
const note = await db.notes.note(props.item.id);
eSendEvent(eOnLoadNote, {
item: {
...note,
content: props.item.rawContent
? {
data: props.item.rawContent || "",
type: "tiptap"
}
: undefined
},
searchResultIndex: index
});
fluidTabsRef.current?.goToPage("editor");
};
return (
<Pressable
style={{
alignSelf: "flex-start",
alignItems: "flex-start",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
borderRadius: 0
}}
onLongPress={async () => {
const note = await db.notes.note(props.item.id);
Properties.present(note);
}}
onPress={async () => openNote()}
>
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: DefaultAppStyles.GAP_SMALL / 2,
flexShrink: 1
}}
>
{props.item.content?.length ? (
<IconButton
name={!expanded ? "chevron-right" : "chevron-down"}
onPress={() => setExpanded((prev) => !prev)}
size={AppFontSize.md + 2}
color={colors.secondary.icon}
style={{
width: 23,
height: 23
}}
/>
) : null}
<Heading
size={AppFontSize.sm}
style={{
flexShrink: 1
}}
>
{props.item.title.map((title, index) => (
<React.Fragment key={props.item.id + index + "-srt"}>
{title.prefix}
<Heading
size={AppFontSize.sm}
style={{
backgroundColor: colors.secondary.accent,
color: colors.secondary.accentForeground
}}
>
{title.match}
</Heading>
{title.suffix}
</React.Fragment>
))}
</Heading>
</View>
{props.item.content?.length ? (
<Paragraph size={AppFontSize.xxs} color={colors.secondary.paragraph}>
{props.item.content.length}
</Paragraph>
) : null}
</View>
{expanded &&
props.item.content.map((content, index) => (
<Pressable
key={props.item.id + index}
style={{
alignSelf: "flex-start",
alignItems: "flex-start",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
borderColor: colors.primary.border,
borderWidth: 0,
borderRadius: 0,
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
width: "100%"
}}
onLongPress={async () => {
const note = await db.notes.note(props.item.id);
Properties.present(note);
}}
onPress={() => {
let activeIndex = 0;
for (let i = 0; i <= index; i++) {
activeIndex += props.item.content[i].length;
}
openNote(activeIndex);
}}
>
<Paragraph>
{content.map((match, index) => (
<React.Fragment key={props.item.id + index + "src"}>
{match.prefix}
<Paragraph
size={AppFontSize.sm}
style={{
backgroundColor: colors.secondary.accent,
color: colors.secondary.accentForeground
}}
>
{match.match}
</Paragraph>
{match.suffix}
</React.Fragment>
))}
</Paragraph>
</Pressable>
))}
</Pressable>
);
};

View File

@@ -150,7 +150,7 @@ export default function List(props: ListProps) {
flex: 1
}}
>
{props.data?.placeholders?.length === 0 || !props.data ? (
{props.data?.placeholders?.length === 0 ? (
<>
{props.CustomLisHeader ? (
props.CustomLisHeader

View File

@@ -27,7 +27,6 @@ import {
GroupHeader,
GroupOptions,
GroupingKey,
HighlightedResult,
Item,
ItemType,
Note,
@@ -50,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 { SearchResult } from "../list-items/search-result";
type ListItemWrapperProps<TItem = Item> = {
group?: GroupingKey;
@@ -155,9 +153,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
},
items?.cacheItem(index) ? 100 : 0
);
} catch (e) {
/** empty */
}
} catch (e) {}
})();
}, [index, items, refreshItem]);
@@ -200,7 +196,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
notebooks={notebooks.current}
reminder={reminder.current}
attachmentsCount={attachmentsCount.current}
date={getDate(item as Note, group)}
date={getDate(item, group)}
isRenderedInActionSheet={isSheet}
index={index}
locked={locked.current}
@@ -231,7 +227,7 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
<NotebookWrapper
item={item as Notebook}
totalNotes={totalNotes.current}
date={getDate(item as Notebook, group)}
date={getDate(item, group)}
index={index}
/>
</>
@@ -289,34 +285,12 @@ export function ListItemWrapper(props: ListItemWrapperProps) {
/>
</>
);
case "searchResult":
return (
<>
{groupHeader && previousIndex.current === index && !isSheet ? (
<SectionHeader
screen={props.renderedInRoute}
item={groupHeader}
index={index}
dataType={item.type}
color={props.customAccentColor}
groupOptions={groupOptions}
onOpenJumpToDialog={() => {
eSendEvent(eOpenJumpToDialog, {
ref: props.scrollRef,
data: items
});
}}
/>
) : null}
<SearchResult item={item as HighlightedResult} />
</>
);
default:
return null;
}
}
function getDate(item: Notebook | Note, groupType?: GroupingKey): number {
function getDate(item: Item, groupType?: GroupingKey): number {
return (
getSortValue(
groupType

View File

@@ -30,10 +30,6 @@ import { fluidTabsRef } from "../../utils/global-refs";
import { AppFontSize } from "../../utils/size";
import { useSideBarDraggingStore } from "../side-menu/dragging-store";
import { IconButton } from "../ui/icon-button";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../sheets/paywall";
import { strings } from "@notesnook/intl";
import { ToastManager } from "../../services/event-manager";
interface ReorderableListProps<T extends { id: string }>
extends Omit<DraxListProps<T>, "renderItem" | "data" | "renderItemContent"> {
@@ -63,9 +59,6 @@ function ReorderableList<T extends { id: string }>({
const [hiddenItemsState, setHiddenItems] = useState(hiddenItems);
const dragging = useSideBarDraggingStore((state) => state.dragging);
const listRef = useRef<FlatList | null>(null);
const customizableSidebarFeature = useIsFeatureAvailable(
"customizableSidebar"
);
if (dragging) {
fluidTabsRef.current?.lock();
@@ -132,7 +125,6 @@ function ReorderableList<T extends { id: string }>({
);
function getOrderedItems() {
if (!customizableSidebarFeature?.isAllowed) return data;
const items: T[] = [];
itemOrderState.forEach((id) => {
const item = data.find((i) => i.id === id);
@@ -166,39 +158,15 @@ function ReorderableList<T extends { id: string }>({
}
}}
longPressDelay={500}
onItemDragStart={async () => {
if (
customizableSidebarFeature &&
!customizableSidebarFeature?.isAllowed
) {
ToastManager.show({
message: customizableSidebarFeature?.error,
type: "info",
actionText: strings.upgrade(),
func: () => PaywallSheet.present(customizableSidebarFeature)
});
return;
}
onItemDragStart={() =>
useSideBarDraggingStore.setState({
dragging: true
});
}}
})
}
disableVirtualization
itemsDraggable={disableDefaultDrag ? dragging : true}
lockItemDragsToMainAxis
onItemReorder={async ({ fromIndex, fromItem, toIndex, toItem }) => {
if (
customizableSidebarFeature &&
!customizableSidebarFeature?.isAllowed
) {
ToastManager.show({
message: customizableSidebarFeature.error,
type: "info",
actionText: strings.upgrade(),
func: () => PaywallSheet.present(customizableSidebarFeature)
});
return;
}
onItemReorder={({ fromIndex, fromItem, toIndex, toItem }) => {
const newOrder = getOrderedItems().map((item) => item.id);
const element = newOrder.splice(fromIndex, 1)[0];
if (toIndex === 0) {

View File

@@ -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>
);
};

View File

@@ -117,8 +117,7 @@ export default function NotePreview({ session, content, note }) {
{!session?.locked && !locked ? (
<View
style={{
flex: 1,
backgroundColor: colors.primary.background
flex: 1
}}
>
<ReadonlyEditor

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,96 @@
/*
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 { FeatureBlock } from "./feature";
import { ScrollView } from "react-native-actions-sheet";
import { DefaultAppStyles } from "../../utils/styles";
export const CompactFeatures = ({
vertical,
features = [],
maxHeight = 600,
scrollRef
}) => {
let data = vertical
? features
: [
{
highlight: "Everything",
content: "in basic",
icon: "emoticon-wink"
},
{
highlight: "Unlimited",
content: "notebooks",
icon: "notebook"
},
{
highlight: "File & image",
content: "attachments",
icon: "attachment"
},
{
highlight: "Instant",
content: "syncing",
icon: "sync"
},
{
highlight: "Private",
content: "vault",
icon: "shield"
},
{
highlight: "Daily, weekly & monthly",
content: "recurring reminders",
icon: "bell"
},
{
highlight: "Rich text",
content: "editing",
icon: "square-edit-outline"
},
{
highlight: "PDF & markdown",
content: "exports",
icon: "file"
},
{
highlight: "Encrypted",
content: "backups",
icon: "backup-restore"
}
];
return (
<ScrollView
horizontal={!vertical}
showsHorizontalScrollIndicator={false}
style={{
width: "100%",
maxHeight: maxHeight,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{data.map((item) => (
<FeatureBlock key={item.highlight} vertical={vertical} {...item} />
))}
</ScrollView>
);
};

View File

@@ -0,0 +1,290 @@
/*
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, { 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 Navigation from "../../services/navigation";
import { useUserStore } from "../../stores/use-user-store";
import { getElevationStyle } from "../../utils/elevation";
import { eClosePremiumDialog, eCloseSheet } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { AuthMode } from "../auth/common";
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";
import { DefaultAppStyles } from "../../utils/styles";
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();
Navigation.navigate("Auth", {
mode: AuthMode.trialSignup
});
}
};
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={AppFontSize.lg}
style={{
alignSelf: "center",
paddingTop: 20
}}
>
Notesnook{" "}
<Heading size={AppFontSize.lg} color={colors.primary.accent}>
Pro
</Heading>
</Heading>
{!pricing ? (
<ActivityIndicator
style={{
marginBottom: 20
}}
size={AppFontSize.md}
color={colors.primary.accent}
/>
) : (
<Paragraph
style={{
alignSelf: "center",
marginBottom: 20
}}
size={AppFontSize.md}
>
(
{Platform.OS === "android"
? pricing.product?.subscriptionOfferDetails[0]?.pricingPhases
.pricingPhaseList?.[0].formattedPrice
: pricing.product?.localizedPrice}{" "}
/ mo)
</Paragraph>
)}
<Paragraph
key="description"
size={AppFontSize.md}
style={{
paddingHorizontal: DefaultAppStyles.GAP,
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: DefaultAppStyles.GAP,
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: DefaultAppStyles.GAP,
marginBottom: 15,
borderRadius: 100
}}
/>
{!user || userCanRequestTrial ? (
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xs}
style={{
alignSelf: "center",
textAlign: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
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={AppFontSize.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: DefaultAppStyles.GAP
}}
>
<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: DefaultAppStyles.GAP * 2,
position: "absolute",
borderRadius: 100,
bottom: 30,
...getElevationStyle(10)
}}
/>
) : null}
<Toast context="local" />
<View
style={{
paddingBottom: 10
}}
/>
</View>
);
};

View File

@@ -0,0 +1,218 @@
/*
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, { useEffect, useState } from "react";
import { View } from "react-native";
import { usePricing } from "../../hooks/use-pricing";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import PremiumService from "../../services/premium";
import { useThemeColors } from "@notesnook/theme";
import {
eOpenPremiumDialog,
eOpenResultDialog,
eOpenTrialEndingDialog
} from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
import DialogContainer from "../dialog/dialog-container";
import { Button } from "../ui/button";
import Seperator from "../ui/seperator";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { CompactFeatures } from "./compact-features";
import { Offer } from "./offer";
import { DefaultAppStyles } from "../../utils/styles";
export const Expiring = () => {
const { colors } = useThemeColors();
const [visible, setVisible] = useState(false);
const [status, setStatus] = useState({
title: "Your trial is ending soon",
offer: "Get 30% off",
extend: true
});
const pricing = usePricing("yearly");
const promo =
status.offer && pricing?.info
? {
promoCode:
pricing?.info?.discount > 30
? pricing.info.sku
: "com.streetwriters.notesnook.sub.yr.trialoffer",
text: `GET ${
pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}% OFF on yearly`,
discount: pricing?.info?.discount > 30 ? pricing?.info?.discount : 30
}
: null;
useEffect(() => {
eSubscribeEvent(eOpenTrialEndingDialog, open);
return () => {
eUnSubscribeEvent(eOpenTrialEndingDialog, open);
};
}, []);
const open = (status) => {
setStatus(status);
setVisible(true);
};
return (
visible && (
<BaseDialog
onRequestClose={() => {
setVisible(false);
}}
>
<DialogContainer>
<View
style={{
width: "100%",
alignItems: "center"
}}
>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
width: "100%"
}}
>
<Heading
textBreakStrategy="balanced"
style={{
textAlign: "center",
paddingTop: 18
}}
>
{status.title}
</Heading>
<Seperator />
<View
style={{
width: "100%",
alignItems: "center"
}}
>
{status.offer ? (
<>
<Offer padding={20} off={promo?.discount || 30} />
</>
) : (
<>
<Paragraph
textBreakStrategy="balanced"
style={{
textAlign: "center",
paddingTop: 0,
paddingBottom: 20
}}
size={AppFontSize.md + 2}
>
Upgrade now to continue using all the pro features after
your trial ends
</Paragraph>
</>
)}
<CompactFeatures />
<Paragraph
onPress={async () => {
setVisible(false);
await sleep(300);
eSendEvent(eOpenPremiumDialog, promo);
}}
size={AppFontSize.xs}
style={{
textDecorationLine: "underline",
color: colors.secondary.paragraph,
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
>
{"See what's included in Basic & Pro plans"}
</Paragraph>
<Seperator />
</View>
</View>
<View
style={{
backgroundColor: colors.secondary.background,
width: "100%",
borderBottomRightRadius: 10,
borderBottomLeftRadius: 10
}}
>
<Button
type="transparent"
title="Subscribe now"
onPress={async () => {
setVisible(false);
await sleep(300);
PremiumService.sheet(
null,
promo?.discount > 30 ? null : promo
);
}}
fontSize={AppFontSize.md + 2}
style={{
marginBottom: status.extend ? 0 : 10,
marginTop: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: DefaultAppStyles.GAP * 2
}}
/>
{status.extend && (
<Button
type="plain"
title="Not sure yet? Extend trial for 7 days"
textStyle={{
textDecorationLine: "underline"
}}
onPress={async () => {
setVisible(false);
await sleep(300);
eSendEvent(eOpenResultDialog, {
title: "Your trial has been extended",
paragraph:
"Try out all features of Notesnook free for 7 more days. No limitations. No commitments.",
button: "Continue"
});
}}
fontSize={AppFontSize.xs}
height={30}
style={{
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
/>
)}
</View>
</View>
</DialogContainer>
</BaseDialog>
)
);
};

View File

@@ -0,0 +1,97 @@
/*
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 { Text, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useThemeColors } from "@notesnook/theme";
import { defaultBorderRadius, AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
import { ProTag } from "./pro-tag";
import { DefaultAppStyles } from "../../utils/styles";
export const FeatureBlock = ({
vertical,
highlight,
content,
icon,
pro,
proTagBg
}) => {
const { colors } = useThemeColors();
return vertical ? (
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
marginBottom: DefaultAppStyles.GAP_VERTICAL,
backgroundColor: colors.secondary.background,
borderRadius: 10,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph
style={{
flexWrap: "wrap",
marginLeft: 5,
flexShrink: 1
}}
size={AppFontSize.sm}
>
{content}
</Paragraph>
</View>
) : (
<View
style={{
height: 100,
justifyContent: "center",
padding: DefaultAppStyles.GAP_SMALL,
marginRight: 10,
borderRadius: defaultBorderRadius,
minWidth: 100
}}
>
<Icon color={colors.primary.icon} name={icon} size={AppFontSize.xl} />
<Paragraph size={AppFontSize.md}>
<Text style={{ color: colors.primary.accent }}>{highlight}</Text>
{content ? "\n" + content : null}
</Paragraph>
{pro ? (
<>
<View style={{ height: 5 }} />
<ProTag width={50} size={AppFontSize.xs} background={proTagBg} />
</>
) : (
<View
style={{
width: 30,
height: 3,
marginTop: DefaultAppStyles.GAP_VERTICAL,
borderRadius: 100,
backgroundColor: colors.primary.accent
}}
/>
)}
</View>
);
};

View File

@@ -0,0 +1,318 @@
/*
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 features = [
{
title: "Focused on privacy",
detail:
"Everything you do in Notesnook stays private. We use XChaCha20-Poly1305-IETF and Argon2 to encrypt your notes.",
features: [
{
highlight: "Zero ads",
content: "& zero trackers",
icon: "billboard"
},
{
highlight: "On device",
content: "encryption",
icon: "cellphone"
},
{
highlight: "Secure app",
content: "lock for all",
icon: "cellphone-lock"
},
{
highlight: "100% end-to-end ",
content: "encrypted",
icon: "lock"
},
{
highlight: "Password protected",
content: "notes sharing",
icon: "file-lock"
}
]
},
{
title: "No limit on notes or devices",
detail:
"Basic or Pro, you can create unlimited number of notes and access them on all your devices. You won't be running out of space or blocks ever."
},
{
title: "Attach files & images",
detail:
"Add your documents, PDFs, images and videos, and keep them safe and organized.",
pro: true,
features: [
{
highlight: "Bullet proof",
content: "encryption",
icon: "lock"
},
{
highlight: "High quality",
content: "4k images",
icon: "image-multiple"
},
{
highlight: "No monthly",
content: "storage limit",
icon: "harddisk"
},
{
highlight: "Generous 500 MB",
content: "max file size",
icon: "file-cabinet"
},
{
highlight: "No restriction",
content: "on file type",
icon: "file"
}
]
},
{
title: "Cross platform Reminders",
detail: "Stay updated on all your upcoming tasks with reminders.",
features: [
{
highlight: "One-time",
content: "reminders",
icon: "bell"
},
{
highlight: "Daily, weekly & monthly",
content: "reminders",
icon: "refresh",
pro: true
}
]
},
{
title: "Two-factor authentication",
detail:
"Improve account security & prevent intruders from accessing your notes",
info: "* 2FA via email is enabled by default for all users.",
features: [
{
highlight: "Email *",
icon: "bell"
},
{
highlight: "Authentication",
content: "app",
icon: "refresh"
},
{
highlight: "SMS",
icon: "refresh",
pro: true
}
]
},
{
title: "Keep secrets always locked with private vault",
detail:
"An extra layer of security for any important data. Notes in the vault always stay encrypted and require a password to be accessed or edited everytime.",
pro: true
},
{
title: "Organize yourself in the best way",
detail:
"We offer multiple ways to keep you organized. The only limit is your imagination.",
features: [
{
highlight: "Unlimited",
content: "notebooks & tags*",
icon: "emoticon",
pro: true
},
{
highlight: "Organize",
content: "with colors",
icon: "palette",
pro: true
},
{
highlight: "Side menu",
content: "shortcuts",
icon: "link-variant"
},
{
highlight: "Pin note in",
content: "notifications",
icon: "pin",
platform: "android"
}
],
info: "* Free users are limited to keeping 3 notebooks and 5 tags."
},
{
title: "Instant sync",
detail:
"Seamlessly work from anywhere on any device. Every change is synced instantly to all your devices.",
info: "* Disable sync completely, turn off auto sync or disable editor realtime sync.",
features: [
{
highlight: "Sync to unlimited",
content: "devices",
icon: "cellphone"
},
{
highlight: "Realtime",
content: "editor sync",
icon: "sync"
},
{
highlight: "Granular sync",
content: "controls *",
icon: "sync-off"
}
]
},
{
title: "Rich tools for rich editing",
detail:
"Having the right tool at the right time is crucial for note taking. Lists, tables, codeblocks — you name it, we have it.",
features: [
{
highlight: "Basic formatting",
content: "and lists",
icon: "format-bold"
},
{
highlight: "Checklists",
content: "& tables",
icon: "table",
pro: true
},
{
highlight: "Markdown",
content: "support",
icon: "language-markdown",
pro: true
},
{
highlight: "Personalized",
content: "editor toolbar",
icon: "gesture-tap-button",
pro: true
},
{
highlight: "Write notes from",
content: "notifications",
icon: "bell",
platform: "android"
}
]
},
{
title: "Safe publishing to the Internet",
detail:
"Publishing is nothing new but we offer fully encrypted, anonymous publishing. Take any note & share it with the world.",
features: [
{
highlight: "Password protected",
content: "sharing",
icon: "send-lock"
},
{
highlight: "Self destruct",
content: "monographs",
icon: "bomb"
}
]
},
{
title: "Export and take your notes anywhere",
pro: true,
detail:
"You own your notes, not us. No proprietary formats. No vendor lock in. No waiting for hours to download your notes.",
info: "* Free users can export notes in well formatted plain text.",
features: [
{
highlight: "Export as ",
content: "Markdown",
icon: "language-markdown",
pro: true
},
{
highlight: "Export as",
content: "PDF",
icon: "file-pdf-box",
pro: true
},
{
highlight: "Export as",
content: "HTML",
icon: "language-html5",
pro: true
},
{
highlight: "Export as",
content: "text",
icon: "clipboard-text-outline"
}
]
},
{
title: "Backup & keep your notes safe",
detail:
"Do not worry about losing your data. Turn on automatic backups on weekly or daily basis.",
features: [
{
highlight: "Backup",
content: "encryption",
icon: "backup-restore"
}
],
pro: true
},
{
title: "Personalize & make Notesnook your own",
detail:
"Change app themes to match your style. Custom themes are coming soon.",
features: [
{
highlight: "Automatic",
content: "dark mode",
icon: "theme-light-dark",
pro: false
},
{
highlight: "Change accent",
content: "color",
icon: "invert-colors",
pro: true
}
]
}
];
/**
*
{
highlight: 'Private vault',
content: 'for notes',
icon: 'shield-lock'
}
*/

View File

@@ -0,0 +1,93 @@
/*
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 { ScrollView, View } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { FeatureBlock } from "./feature";
import { ProTag } from "./pro-tag";
import { DefaultAppStyles } from "../../utils/styles";
export const Group = ({ item, index }) => {
const { colors } = useThemeColors();
return (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
backgroundColor:
index % 2 !== 0
? colors.primary.background
: colors.secondary.background,
paddingVertical: 40
}}
>
{item?.pro ? (
<ProTag
size={AppFontSize.sm}
background={
index % 2 === 0
? colors.primary.background
: colors.secondary.background
}
/>
) : null}
<Heading>{item.title}</Heading>
<Paragraph size={AppFontSize.md}>{item.detail}</Paragraph>
{item.features && (
<ScrollView
style={{
marginTop: DefaultAppStyles.GAP
}}
horizontal
showsHorizontalScrollIndicator={false}
>
{item.features?.map((item) => (
<FeatureBlock
key={item.detail}
{...item}
detail={item.detail}
pro={item.pro}
proTagBg={
index % 2 === 0
? colors.primary.background
: colors.secondary.background
}
/>
))}
</ScrollView>
)}
{item.info ? (
<Paragraph
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
{item.info}
</Paragraph>
) : null}
</View>
);
};

View File

@@ -0,0 +1,87 @@
/*
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, { createRef } from "react";
import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { eClosePremiumDialog, eOpenPremiumDialog } from "../../utils/events";
import BaseDialog from "../dialog/base-dialog";
import { Component } from "./component";
class PremiumDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: false,
promo: null
};
this.actionSheetRef = createRef();
}
componentDidMount() {
eSubscribeEvent(eOpenPremiumDialog, this.open);
eSubscribeEvent(eClosePremiumDialog, this.close);
}
componentWillUnmount() {
eUnSubscribeEvent(eOpenPremiumDialog, this.open);
eUnSubscribeEvent(eClosePremiumDialog, this.close);
}
open = (promoInfo) => {
this.setState({
visible: true,
promo: promoInfo
});
};
close = () => {
this.setState({
visible: false,
promo: null
});
};
onClose = () => {
this.setState({
visible: false
});
};
render() {
return !this.state.visible ? null : (
<BaseDialog
animation="slide"
bounce={false}
background={this.props.colors.primary.background}
onRequestClose={this.onClose}
>
<Component
getRef={() => this.actionSheetRef}
promo={this.state.promo}
close={this.close}
/>
</BaseDialog>
);
}
}
export default PremiumDialog;

View File

@@ -17,22 +17,30 @@ 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 { Check, Cross } from "../../components/icons";
import React from "react";
import { Text } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
export function FeatureCaption({
caption
}: {
caption: boolean | number | string;
}) {
return typeof caption === "boolean" ? (
caption ? (
<Check size={14} />
) : (
<Cross size={14} />
)
) : caption === "infinity" ? (
"∞"
) : (
caption
export const Offer = ({
off = "30",
text = "on yearly plan, offer ends soon",
padding = 0
}) => {
const { colors } = useThemeColors();
return (
<Paragraph
style={{
textAlign: "center",
paddingVertical: padding
}}
size={AppFontSize.xxxl}
>
GET {off}
<Text style={{ color: colors.primary.accent }}>%</Text> OFF!{"\n"}
<Paragraph color={colors.secondary.paragraph}>{text}</Paragraph>
</Paragraph>
);
}
};

View File

@@ -0,0 +1,144 @@
/*
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, { useCallback, useEffect, useRef, useState } from "react";
import { View } from "react-native";
import Animated, { FadeInUp, FadeOutUp } from "react-native-reanimated";
import useKeyboard from "../../hooks/use-keyboard";
import { DDS } from "../../services/device-detection";
import {
eSendEvent,
eSubscribeEvent,
eUnSubscribeEvent
} from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../utils/elevation";
import {
eCloseActionSheet,
eCloseSheet,
eOpenPremiumDialog,
eShowGetPremium
} from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
export const PremiumToast = ({ context = "global", offset = 0 }) => {
const { colors } = useThemeColors();
const [msg, setMsg] = useState(null);
const timer = useRef();
const keyboard = useKeyboard();
const open = useCallback(
(event) => {
if (!event) {
clearTimeout(timer);
timer.current = null;
setMsg(null);
return;
}
if (event.context === context && msg?.desc !== event.desc) {
if (timer.current !== null) {
clearTimeout(timer.current);
timer.current = null;
}
setMsg(event);
timer.current = setTimeout(async () => {
setMsg(null);
}, 3000);
}
},
[context, msg?.desc]
);
useEffect(() => {
eSubscribeEvent(eShowGetPremium, open);
return () => {
eUnSubscribeEvent(eShowGetPremium, open);
};
}, [open]);
const onPress = async () => {
open(null);
eSendEvent(eCloseActionSheet);
eSendEvent(eCloseSheet);
await sleep(300);
eSendEvent(eOpenPremiumDialog);
};
return (
!!msg && (
<Animated.View
entering={FadeInUp}
exiting={FadeOutUp}
style={{
position: "absolute",
backgroundColor: colors.secondary.background,
zIndex: 999,
...getElevationStyle(20),
padding: DefaultAppStyles.GAP,
borderRadius: 10,
flexDirection: "row",
alignSelf: "center",
justifyContent: "space-between",
top: offset + keyboard.keyboardHeight,
maxWidth: DDS.isLargeTablet() ? 400 : "98%"
}}
onTouchEnd={() => {
setMsg(null);
clearTimeout(timer.current);
}}
>
<View
style={{
flexShrink: 1,
flexGrow: 1,
paddingRight: 6
}}
>
<Heading
style={{
flexWrap: "wrap"
}}
color={colors.primary.accent}
size={AppFontSize.md}
>
{msg.title}
</Heading>
<Paragraph
style={{
flexWrap: "wrap"
}}
size={AppFontSize.sm}
color={colors.primary.paragraph}
>
{msg.desc}
</Paragraph>
</View>
<Button onPress={onPress} title="Get Now" type="accent" />
</Animated.View>
)
);
};

View File

@@ -0,0 +1,101 @@
/*
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, View } from "react-native";
import { AppFontSize } from "../../utils/size";
import { Pressable } from "../ui/pressable";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import RNIap from "react-native-iap";
import { DefaultAppStyles } from "../../utils/styles";
export const PricingItem = ({
product,
onPress,
compact,
strikethrough
}: {
product: {
type: "yearly" | "monthly";
data?: RNIap.Subscription;
info: string;
offerType?: "yearly" | "monthly";
};
strikethrough?: boolean;
onPress?: () => void;
compact?: boolean;
}) => {
return (
<Pressable
onPress={onPress}
type="secondary"
style={{
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: compact ? 15 : 10,
width: compact ? null : "100%",
minWidth: 150,
opacity: strikethrough ? 0.7 : 1
}}
disabled={strikethrough}
>
{!compact && (
<View>
<Heading size={AppFontSize.lg - 2}>
{product?.type === "yearly" || product?.offerType === "yearly"
? "Yearly"
: "Monthly"}
</Heading>
{product?.info && (
<Paragraph size={AppFontSize.xs}>{product.info}</Paragraph>
)}
</View>
)}
<View>
<Paragraph
style={{
textDecorationLine: strikethrough ? "line-through" : undefined
}}
size={AppFontSize.sm}
>
<Heading
style={{
textDecorationLine: strikethrough ? "line-through" : undefined
}}
size={AppFontSize.lg - 2}
>
{Platform.OS === "android"
? (product.data as RNIap.SubscriptionAndroid | undefined)
?.subscriptionOfferDetails?.[0]?.pricingPhases
.pricingPhaseList?.[0]?.formattedPrice
: (product.data as RNIap.SubscriptionIOS | undefined)
?.localizedPrice}
</Heading>
{product?.type === "yearly" || product?.offerType === "yearly"
? "/year"
: "/month"}
</Paragraph>
</View>
</Pressable>
);
};

View File

@@ -0,0 +1,735 @@
/*
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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Platform, Text, View } from "react-native";
import * as RNIap from "react-native-iap";
import { DatabaseLogger, db } from "../../common/database";
import { usePricing } from "../../hooks/use-pricing";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../services/event-manager";
import Navigation from "../../services/navigation";
import PremiumService from "../../services/premium";
import { useSettingStore } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store";
import {
eClosePremiumDialog,
eCloseSheet,
eCloseSimpleDialog
} from "../../utils/events";
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { AuthMode } from "../auth/common";
import { Dialog } from "../dialog";
import BaseDialog from "../dialog/base-dialog";
import { presentDialog } from "../dialog/functions";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { Walkthrough } from "../walkthroughs";
import { PricingItem } from "./pricing-item";
import { DefaultAppStyles } from "../../utils/styles";
const UUID_PREFIX = "0bdaea";
const UUID_VERSION = "4";
const UUID_VARIANT = "a";
function toUUID(str: string) {
return [
UUID_PREFIX + str.substring(0, 2), // 6 digit prefix + first 2 oid digits
str.substring(2, 6), // # next 4 oid digits
UUID_VERSION + str.substring(6, 9), // # 1 digit version(0x4) + next 3 oid digits
UUID_VARIANT + str.substring(9, 12), // # 1 digit variant(0b101) + 1 zero bit + next 3 oid digits
str.substring(12)
].join("-");
}
const promoCyclesMonthly = {
1: "first month",
2: "first 2 months",
3: "first 3 months",
4: "first 4 months",
5: "first 5 months",
6: "first 3 months"
};
const promoCyclesYearly = {
1: "first year",
2: "first 2 years",
3: "first 3 years"
};
export const PricingPlans = ({
promo,
marginTop,
heading = true,
compact = false
}: {
promo?: {
promoCode: string;
};
marginTop?: any;
heading?: boolean;
compact?: boolean;
}) => {
const { colors } = useThemeColors();
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);
}
}, [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 as "ios" | "android"
);
}
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) {
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);
}
};
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
style={{
paddingHorizontal: DefaultAppStyles.GAP,
justifyContent: "center",
alignItems: "center",
height: 100
}}
>
<ActivityIndicator color={colors.primary.accent} size={25} />
</View>
) : (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
{buying ? (
<BaseDialog statusBarTranslucent centered>
<ActivityIndicator size={50} color="white" />
</BaseDialog>
) : null}
{!upgrade ? (
<>
<Paragraph
style={{
alignSelf: "center"
}}
size={AppFontSize.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: DefaultAppStyles.GAP,
marginBottom: DefaultAppStyles.GAP,
marginTop: DefaultAppStyles.GAP,
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: DefaultAppStyles.GAP,
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."
}}
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: AppFontSize.lg
}}
>
Special offer for you
</Heading>
<View
style={{
paddingVertical: 20,
paddingBottom: 10
}}
>
<Heading
style={{
alignSelf: "center",
textAlign: "center"
}}
size={AppFontSize.xxl}
>
{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={{
color: colors.secondary.paragraph,
alignSelf: "center",
textAlign: "center"
}}
size={AppFontSize.md}
>
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: DefaultAppStyles.GAP_VERTICAL
}}
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
}}
/>
)}
</>
) : (
<View>
{!user ? (
<>
<Button
onPress={() => {
eSendEvent(eClosePremiumDialog);
eSendEvent(eCloseSheet);
Navigation.navigate("Auth", {
mode: AuthMode.login
});
}}
title={"Sign up for free"}
type="accent"
width={250}
style={{
paddingHorizontal: DefaultAppStyles.GAP,
marginTop: product?.type === "promo" ? 0 : 30,
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
/>
{Platform.OS !== "ios" &&
promo &&
!promo.promoCode.startsWith("com.streetwriters.notesnook") ? (
<Paragraph
size={AppFontSize.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: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
height={30}
fontSize={13}
type="errorShade"
title="Cancel promo code"
/>
</>
)}
</View>
)}
</>
)}
{!user || !upgrade ? (
<Paragraph
color={colors.secondary.paragraph}
size={AppFontSize.xs}
style={{
alignSelf: "center",
textAlign: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
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={AppFontSize.xs} style={{ fontWeight: "bold" }}>
No credit card is required.
</Paragraph>
</Paragraph>
) : null}
{user && upgrade ? (
<>
{Platform.OS === "ios" ? (
<Paragraph
textBreakStrategy="balanced"
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
textAlign: "center"
}}
>
By subscribing, you will be charged to your iTunes Account for the
selected plan. Subscriptions will automatically renew unless
cancelled within 24-hours before the end of the current period.
</Paragraph>
) : (
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
textAlign: "center"
}}
>
By subscribing, you will be charged on your Google Account, and
your subscription will automatically renew until you cancel prior
to the end of the then current period.
</Paragraph>
)}
<View
style={{
width: "100%"
}}
>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
style={{
maxWidth: "100%",
textAlign: "center"
}}
>
By subscribing, you agree to our{" "}
<Paragraph
size={AppFontSize.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos")
.catch(() => {})
.then(() => {});
}}
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
Terms of Service{" "}
</Paragraph>
and{" "}
<Paragraph
size={AppFontSize.xs}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy")
.catch(() => {})
.then(() => {});
}}
style={{
textDecorationLine: "underline"
}}
color={colors.primary.accent}
>
Privacy Policy.
</Paragraph>
</Paragraph>
</View>
</>
) : null}
<Dialog context="local" />
</View>
);
};

View File

@@ -0,0 +1,60 @@
/*
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 { View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useThemeColors } from "@notesnook/theme";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
/**
*
* @param {any} param0
* @returns
*/
export const ProTag = ({ width, size, background }) => {
const { colors } = useThemeColors();
return (
<View
style={{
backgroundColor: background || colors.primary.background,
borderRadius: 100,
width: width || 60,
justifyContent: "center",
alignItems: "center",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL / 2,
flexDirection: "row"
}}
>
<Icon
style={{
marginRight: 3
}}
size={size}
color={colors.primary.accent}
name="crown"
/>
<Paragraph size={size - 1.5} color={colors.primary.accent}>
PRO
</Paragraph>
</View>
);
};

View File

@@ -25,7 +25,7 @@ import { FlashList } from "@shopify/flash-list";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import { eSendEvent } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useRelationStore } from "../../stores/use-relation-store";
@@ -38,8 +38,6 @@ import NativeTooltip from "../../utils/tooltip";
import { Pressable } from "../ui/pressable";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import { isFeatureAvailable, useIsFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../sheets/paywall";
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
const { colors } = useThemeColors();
@@ -75,6 +73,9 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
testID={notesnook.ids.dialogs.actionsheet.color(item.colorCode)}
key={item.id}
onPress={toggleColor}
onLongPress={(event) => {
NativeTooltip.show(event, item.title, NativeTooltip.POSITIONS.TOP);
}}
style={{
width: 35,
height: 35,
@@ -97,7 +98,6 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
};
export const ColorTags = ({ item }: { item: Note }) => {
const colorFeature = useIsFeatureAvailable("colors");
const { colors } = useThemeColors();
const colorNotes = useMenuStore((state) => state.colorNotes);
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
@@ -112,24 +112,6 @@ export const ColorTags = ({ item }: { item: Note }) => {
[note]
);
const onPress = React.useCallback(async () => {
if (colorFeature && !colorFeature.isAllowed) {
ToastManager.show({
message: colorFeature.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(colorFeature);
ToastManager.hide();
}
});
return;
}
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}, []);
return (
<>
<ColorPicker
@@ -154,7 +136,10 @@ export const ColorTags = ({ item }: { item: Note }) => {
>
{!colorNotes || !colorNotes.length ? (
<Button
onPress={onPress}
onPress={async () => {
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
buttonType={{
text: colors.primary.accent
}}
@@ -190,7 +175,10 @@ export const ColorTags = ({ item }: { item: Note }) => {
marginRight: 5
}}
type="secondary"
onPress={onPress}
onPress={() => {
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
>
<Icon
testID="icon-plus"

View File

@@ -21,14 +21,13 @@ import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { FlatList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { ColorValues } from "../../utils/colors";
import { eOnLoadNote } from "../../utils/events";
import { fluidTabsRef } from "../../utils/global-refs";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import SheetProvider from "../sheet-provider";
import { IconButton } from "../ui/icon-button";
import { Pressable } from "../ui/pressable";
@@ -39,6 +38,8 @@ import { DateMeta } from "./date-meta";
import { Items } from "./items";
import Notebooks from "./notebooks";
import { TagStrip, Tags } from "./tags";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { DefaultAppStyles } from "../../utils/styles";
const Line = ({ top = 6, bottom = 6 }) => {
const { colors } = useThemeColors();
@@ -150,8 +151,7 @@ export const Properties = ({ close = () => {}, item, buttons = [] }) => {
) : null}
</View>
{(item.type === "notebook" || item.type === "reminder") &&
item.description ? (
{item.type === "notebook" && item.description ? (
<Paragraph>{item.description}</Paragraph>
) : null}

View File

@@ -27,9 +27,8 @@ import { Action, ActionId, useActions } from "../../hooks/use-actions";
import { useStoredRef } from "../../hooks/use-stored-ref";
import { DDS } from "../../services/device-detection";
import { useSettingStore } from "../../stores/use-setting-store";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import AppIcon from "../ui/AppIcon";
import { Button } from "../ui/button";
import { Pressable } from "../ui/pressable";
import Paragraph from "../ui/typography/paragraph";
@@ -66,11 +65,8 @@ const COLUMN_BAR_ITEMS: ActionId[] = [
"edit-notebook",
"move-notes",
"move-notebook",
"edit-reminder",
"pin",
"disable-reminder",
"default-notebook",
"default-tag",
"default-homepage",
"add-shortcut",
"reorder",
@@ -146,8 +142,7 @@ export const Items = ({
key={item.id}
style={{
alignItems: "center",
width: columnItemWidth - 8,
opacity: item.locked ? 0.5 : 1
width: columnItemWidth - 8
}}
>
<Pressable
@@ -222,8 +217,7 @@ export const Items = ({
borderRadius: 0,
justifyContent: "flex-start",
alignSelf: "flex-start",
width: "100%",
opacity: item.locked ? 0.5 : 1
width: "100%"
}}
/>
),
@@ -247,9 +241,7 @@ export const Items = ({
key={item.id}
testID={"icon-" + item.id}
style={{
width: columnItemWidth - 8,
alignSelf: "flex-start",
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
width: columnItemWidth - 8
}}
>
<View
@@ -257,13 +249,7 @@ export const Items = ({
height: columnItemWidth / 2,
width: columnItemWidth - DefaultAppStyles.GAP_SMALL,
justifyContent: "center",
alignItems: "center",
borderWidth: 1,
borderRadius: defaultBorderRadius,
borderColor: item.checked
? item.activeColor || colors.primary.accent
: colors.primary.border,
overflow: "hidden"
alignItems: "center"
}}
>
<Icon
@@ -278,28 +264,6 @@ export const Items = ({
: colors.secondary.icon
}
/>
{item.locked ? (
<View
style={{
width: 20,
height: 20,
borderRadius: 100,
backgroundColor: colors.primary.accent,
justifyContent: "center",
alignItems: "center",
position: "absolute",
bottom: -3,
right: -3
}}
>
<AppIcon
color={colors.static.orange}
size={AppFontSize.xxxs}
name="crown"
/>
</View>
) : null}
</View>
<Paragraph
@@ -357,21 +321,16 @@ export const Items = ({
paginationStyle={{
position: "relative",
marginHorizontal: 2,
marginBottom: -10,
marginTop: 10
marginBottom: 0,
marginTop: DefaultAppStyles.GAP
}}
contentContainerStyle={{
justifyContent: "flex-start",
alignItems: "flex-start",
alignSelf: "flex-start"
}}
centerContent={false}
renderItem={({ item, index }) => (
<View
style={{
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP,
gap: 5
gap: 5,
width: width
}}
>
{item.map(renderTopBarItem)}

View File

@@ -17,18 +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 { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import ManageTags from "../../screens/manage-tags";
import { TaggedNotes } from "../../screens/notes/tagged";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
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";
import { DefaultAppStyles } from "../../utils/styles";
export const Tags = ({ item, close }) => {
const { colors } = useThemeColors();
@@ -47,8 +47,7 @@ export const Tags = ({ item, close }) => {
>
<Button
onPress={async () => {
ManageTags.present([item.id]);
close();
ManageTagsSheet.present([item.id]);
}}
buttonType={{
text: colors.primary.accent
@@ -86,6 +85,7 @@ export const TagStrip = ({ item, close }) => {
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
gap: 5
}}
>
@@ -106,13 +106,16 @@ const TagItem = ({ tag, close }) => {
const style = {
paddingHorizontal: 0,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
borderRadius: 100,
marginTop: 0,
backgroundColor: "transparent"
};
return (
<Button
onPress={onPress}
title={"#" + tag.title}
type="plain"
height={20}
fontSize={AppFontSize.xs}
style={style}
textStyle={{

View File

@@ -42,11 +42,10 @@ import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { presentDialog } from "../dialog/functions";
import ExportNotesSheet from "../sheets/export-notes";
import ManageTagsSheet from "../sheets/manage-tags";
import { MoveNotebook } from "../../screens/move-notebook";
import { IconButton } from "../ui/icon-button";
import NativeTooltip from "../../utils/tooltip";
import ManageTags from "../../screens/manage-tags";
export const SelectionHeader = React.memo(
({
@@ -87,7 +86,7 @@ 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();
@@ -197,10 +196,20 @@ export const SelectionHeader = React.memo(
}
]
: [
{
title: strings.move(),
onPress: async () => {
const ids = selectedItemsList;
const notebooks = await db.notebooks.all.items(ids);
MoveNotebook.present(notebooks);
},
visible: renderedInRoute === "Notebooks",
icon: "arrow-right-bold-box-outline"
},
{
title: strings.manageTags(),
onPress: async () => {
ManageTags.present(selectedItemsList);
ManageTagsSheet.present(selectedItemsList);
},
visible: type === "note",
icon: "pound"

View File

@@ -95,6 +95,8 @@ const SheetProvider = ({ context = "global" }) => {
[context]
);
console.log(data?.keyboardHandlerDisabled);
return !visible || !data ? null : (
<SheetWrapper
fwdRef={actionSheetRef}
@@ -224,6 +226,7 @@ const SheetProvider = ({ context = "global" }) => {
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
width="100%"
fontSize={AppFontSize.md}
/>
))}

View File

@@ -24,22 +24,23 @@ import { TextInput, View } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { db } from "../../../common/database";
import { DDS } from "../../../services/device-detection";
import { ToastManager, eSendEvent } from "../../../services/event-manager";
import {
ToastManager,
eSendEvent,
presentSheet
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useMenuStore } from "../../../stores/use-menu-store";
import { useNotebookStore } from "../../../stores/use-notebook-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { eOnNotebookUpdated } from "../../../utils/events";
import { getParentNotebookId } from "../../../utils/notebooks";
import { AppFontSize } from "../../../utils/size";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { useSettingStore } from "../../../stores/use-setting-store";
import DialogHeader from "../../dialog/dialog-header";
import DialogButtons from "../../dialog/dialog-buttons";
import { presentDialog } from "../../dialog/functions";
import { getElevationStyle } from "../../../utils/elevation";
import { defaultBorderRadius } from "../../../utils/size";
import { useThemeColors } from "@notesnook/theme";
import { getContainerBorder } from "../../../utils/colors";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import { DefaultAppStyles } from "../../../utils/styles";
export const AddNotebookSheet = ({
notebook,
@@ -54,7 +55,6 @@ export const AddNotebookSheet = ({
showMoveNotesOnComplete: boolean;
defaultTitle?: string;
}) => {
const { colors } = useThemeColors();
const title = useRef(notebook?.title || defaultTitle);
const description = useRef(notebook?.description);
const titleInput = useRef<TextInput>(null);
@@ -124,66 +124,57 @@ export const AddNotebookSheet = ({
return (
<View
style={{
...getElevationStyle(5),
width: DDS.isTab ? 400 : "85%",
maxHeight: 450,
borderRadius: defaultBorderRadius,
backgroundColor: colors.primary.background,
paddingTop: 12,
...getContainerBorder(colors.primary.border, 0.5),
overflow: "hidden"
maxHeight: DDS.isTab ? "90%" : "97%",
borderRadius: DDS.isTab ? 5 : 0,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<View
<Heading size={AppFontSize.lg}>
{notebook ? strings.editNotebook() : strings.newNotebook()}
</Heading>
<Seperator />
<Input
fwdRef={titleInput}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
title.current = value;
}}
onLayout={() => {
setImmediate(() => {
titleInput?.current?.focus();
});
}}
placeholder={strings.enterNotebookTitle()}
onSubmit={() => {
descriptionInput.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : title.current}
/>
<Input
fwdRef={descriptionInput}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
description.current = value;
}}
placeholder={strings.enterNotebookDescription()}
returnKeyLabel={strings.next()}
returnKeyType="next"
defaultValue={notebook ? notebook.description : ""}
/>
<Button
title={notebook ? strings.save() : strings.add()}
type="accent"
style={{
paddingHorizontal: 12
paddingHorizontal: DefaultAppStyles.GAP * 2,
width: "100%"
}}
>
<DialogHeader
title={notebook ? strings.editNotebook() : strings.newNotebook()}
/>
<Input
fwdRef={titleInput}
testID={notesnook.ids.dialogs.notebook.inputs.title}
onChangeText={(value) => {
title.current = value;
}}
onLayout={() => {
setTimeout(() => {
titleInput?.current?.focus();
}, 300);
}}
placeholder={strings.enterNotebookTitle()}
onSubmit={() => {
descriptionInput.current?.focus();
}}
returnKeyLabel="Next"
returnKeyType="next"
defaultValue={notebook ? notebook.title : title.current}
/>
<Input
fwdRef={descriptionInput}
testID={notesnook.ids.dialogs.notebook.inputs.description}
onChangeText={(value) => {
description.current = value;
}}
placeholder={strings.enterNotebookDescription()}
returnKeyLabel={strings.next()}
returnKeyType="next"
defaultValue={notebook ? notebook.description : ""}
/>
</View>
<DialogButtons
onPressNegative={() => {
title.current = undefined;
close?.(false);
useSettingStore.getState().setSheetKeyboardHandler(true);
}}
positiveTitle={notebook ? strings.save() : strings.add()}
negativeTitle={strings.cancel()}
onPressPositive={onSaveChanges}
onPress={onSaveChanges}
/>
</View>
);
@@ -197,9 +188,9 @@ AddNotebookSheet.present = (
showMoveNotesOnComplete = true,
defaultTitle?: string
) => {
presentDialog({
presentSheet({
context: context,
component: (close) => (
component: (ref, close) => (
<AddNotebookSheet
notebook={notebook}
parentNotebook={parentNotebook}

View File

@@ -1,495 +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 { Plan, SKUResponse } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import dayjs from "dayjs";
import React, { useEffect, useState } from "react";
import {
Linking,
ScrollView,
Text,
TouchableOpacity,
View
} from "react-native";
import Config from "react-native-config";
import * as RNIap from "react-native-iap";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { WebView } from "react-native-webview";
import { db } from "../../../common/database";
import usePricingPlans from "../../../hooks/use-pricing-plans";
import { ToastManager } from "../../../services/event-manager";
import { openLinkInBrowser } from "../../../utils/functions";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { Button } from "../../ui/button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const isGithubRelease = Config.GITHUB_RELEASE === "true";
export const BuyPlan = (props: {
planId: string;
canActivateTrial?: boolean;
pricingPlans: ReturnType<typeof usePricingPlans>;
}) => {
const { colors } = useThemeColors();
const [checkoutUrl, setCheckoutUrl] = useState<string>();
const pricingPlans = props.pricingPlans;
const billingDuration = pricingPlans.getBillingDuration(
pricingPlans.selectedProduct as RNIap.Subscription,
0,
0,
true
);
const is5YearPlanSelected = (
isGithubRelease
? (pricingPlans.selectedProduct as Plan)?.period
: (pricingPlans.selectedProduct as RNIap.Product)?.productId
)?.includes("5");
return checkoutUrl ? (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph>{strings.finishPurchaseInBrowser()}</Paragraph>
<Button
title={strings.next()}
type="accent"
onPress={() => {
pricingPlans.finish();
}}
/>
<Button
title={strings.goBack()}
onPress={() => {
setCheckoutUrl(undefined);
}}
/>
</View>
) : (
<ScrollView
contentContainerStyle={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
keyboardDismissMode="none"
keyboardShouldPersistTaps="always"
>
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
{[
Config.GITHUB_RELEASE === "true"
? "yearly"
: `notesnook.${props.planId}.yearly`,
Config.GITHUB_RELEASE === "true"
? "monthly"
: `notesnook.${props.planId}.monthly`,
...(props.planId === "essential" || pricingPlans.isSubscribed()
? []
: [
Config.GITHUB_RELEASE === "true"
? "5-year"
: `notesnook.${props.planId}.5year`
])
].map((item) => (
<ProductItem
key={item}
pricingPlans={pricingPlans}
productId={item}
/>
))}
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
borderWidth: 1,
borderColor: colors.primary.border,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius
}}
>
<Heading color={colors.primary.paragraph} size={AppFontSize.sm}>
{strings.dueToday()}{" "}
{pricingPlans.hasTrialOffer(
props.planId,
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans?.selectedProduct as Plan)?.period
) ? (
<Text
style={{
color: colors.primary.accent
}}
>
({strings.daysFree(`${billingDuration?.duration || 0}`)})
</Text>
) : null}
</Heading>
<Paragraph color={colors.primary.paragraph}>
{pricingPlans.hasTrialOffer(
props.planId,
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans?.selectedProduct as Plan)?.period
)
? "FREE"
: pricingPlans.getStandardPrice(
pricingPlans.selectedProduct as RNIap.Subscription
)}
</Paragraph>
</View>
{pricingPlans.hasTrialOffer(
props.planId,
(pricingPlans?.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans?.selectedProduct as Plan)?.period
) ? (
<View
style={{
flexDirection: "row",
justifyContent: "space-between",
borderWidth: 1,
borderColor: colors.primary.border,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius
}}
>
<Paragraph color={colors.secondary.paragraph}>
{strings.due(
dayjs()
.add(billingDuration?.duration || 0, "day")
.format("DD MMMM")
)}
</Paragraph>
<Paragraph color={colors.secondary.paragraph}>
{pricingPlans.getStandardPrice(
pricingPlans.selectedProduct as RNIap.Subscription
)}
</Paragraph>
</View>
) : null}
{pricingPlans.hasTrialOffer(
props.planId,
(pricingPlans.selectedProduct as RNIap.Product)?.productId ||
(pricingPlans.selectedProduct as Plan)?.period
) || is5YearPlanSelected ? (
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL,
borderWidth: 1,
borderColor: colors.primary.border,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius
}}
>
{(is5YearPlanSelected
? strings["5yearPlanConditions"]()
: [
strings.trialPlanConditions[0](
billingDuration?.duration as number
),
strings.trialPlanConditions[1](0)
]
).map((item) => (
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 10
}}
key={item}
>
<Icon
color={colors.primary.accent}
size={AppFontSize.lg}
name="check"
/>
<Paragraph>{item}</Paragraph>
</View>
))}
</View>
) : null}
<Button
width="100%"
type="accent"
loading={pricingPlans.loading}
title={
is5YearPlanSelected
? strings.purchase()
: pricingPlans?.userCanRequestTrial
? strings.subscribeAndStartTrial()
: strings.subscribe()
}
onPress={async () => {
if (isGithubRelease) {
const url = await db.subscriptions.checkoutUrl(
(pricingPlans.selectedProduct as Plan).plan,
(pricingPlans.selectedProduct as Plan).period
);
if (url) {
setCheckoutUrl(url);
Linking.openURL(url);
}
return;
}
const offerToken = pricingPlans.getOfferTokenAndroid(
pricingPlans.selectedProduct as RNIap.SubscriptionAndroid,
0
);
pricingPlans.subscribe(
pricingPlans.selectedProduct as RNIap.Subscription,
offerToken
);
}}
/>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
size={AppFontSize.xs}
>
{is5YearPlanSelected
? strings.oneTimePurchase()
: strings.cancelAnytimeAlt()}
</Paragraph>
<Paragraph
style={{
textAlign: "center"
}}
color={colors.secondary.paragraph}
size={AppFontSize.xs}
>
{strings.subTerms[0]()}{" "}
<Text
style={{
textDecorationLine: "underline"
}}
onPress={() => {
openLinkInBrowser("https://notesnook.com/privacy");
}}
>
{strings.subTerms[1]()}
</Text>{" "}
{strings.subTerms[2]()}{" "}
<Text
style={{
textDecorationLine: "underline"
}}
onPress={() => {
openLinkInBrowser("https://notesnook.com/tos");
}}
>
{strings.subTerms[3]()}
</Text>
</Paragraph>
</View>
</ScrollView>
);
};
const ProductItem = (props: {
pricingPlans: ReturnType<typeof usePricingPlans>;
productId: string;
}) => {
const { colors } = useThemeColors();
const [regionalDiscount, setRegionaDiscount] = useState<SKUResponse>();
const product =
props.pricingPlans?.currentPlan?.subscriptions?.[
regionalDiscount?.sku || props.productId
] ||
props.pricingPlans?.currentPlan?.products?.[props.productId] ||
props.pricingPlans?.getWebPlan(
props.pricingPlans?.currentPlan?.id as string,
props.productId as "monthly" | "yearly"
);
const isAnnual = isGithubRelease
? (product as Plan)?.period === "yearly"
: (product as RNIap.Subscription)?.productId?.includes("yearly");
const isSelected = isGithubRelease
? (product as Plan)?.period ===
(props.pricingPlans.selectedProduct as Plan)?.period
: (product as RNIap.Subscription)?.productId ===
(props.pricingPlans.selectedProduct as RNIap.Subscription)?.productId;
const is5YearProduct = (
isGithubRelease
? (product as Plan)?.period
: (product as RNIap.Product)?.productId
)?.includes("5");
const isSubscribed =
props.pricingPlans.isSubscribed() &&
(props.pricingPlans.user?.subscription?.productId ===
(product as RNIap.Subscription)?.productId ||
props.pricingPlans.user?.subscription?.productId.startsWith(
(product as RNIap.Subscription)?.productId
) ||
props.pricingPlans.user?.subscription?.productId ===
(product as Plan).id);
useEffect(() => {
props.pricingPlans
?.getRegionalDiscount(
props.pricingPlans.currentPlan?.id as string,
props.pricingPlans.isGithubRelease
? ((product as Plan)?.period as string)
: props.productId
)
.then((value) => {
if (
value &&
value.sku?.startsWith(
(props.pricingPlans.selectedProduct as RNIap.Subscription)
?.productId
)
) {
props.pricingPlans.selectProduct(value?.sku as string);
}
setRegionaDiscount(value);
});
}, []);
return (
<TouchableOpacity
style={{
flexDirection: "row",
gap: 10,
opacity: isSubscribed ? 0.5 : 1
}}
activeOpacity={0.9}
onPress={() => {
if (isSubscribed) {
ToastManager.show({
message: strings.alreadySubscribed(),
type: "info"
});
return;
}
if (!product) return;
props.pricingPlans.selectProduct(
isGithubRelease
? (product as Plan)?.period
: (product as RNIap.Subscription)?.productId
);
}}
>
<Icon
name={isSelected ? "radiobox-marked" : "radiobox-blank"}
color={isSelected ? colors.primary.accent : colors.secondary.icon}
size={AppFontSize.lg}
/>
<View>
<View
style={{
flexDirection: "row",
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
<Heading size={AppFontSize.md}>
{isAnnual
? strings.yearly()
: is5YearProduct
? strings.fiveYearPlan()
: strings.monthly()}
</Heading>
{(isAnnual && !isGithubRelease) ||
(isGithubRelease && (product as Plan)?.discount?.amount) ? (
<View
style={{
backgroundColor: colors.static.red,
borderRadius: defaultBorderRadius,
paddingHorizontal: 6,
alignItems: "center",
justifyContent: "center"
}}
>
<Heading color={colors.static.white} size={AppFontSize.xs}>
{strings.bestValue()} -{" "}
{strings.percentOff(
(regionalDiscount
? regionalDiscount.discount
: isGithubRelease
? (product as Plan).discount?.amount
: props.pricingPlans.compareProductPrice(
props.pricingPlans.currentPlan?.id as string,
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
)) as string
)}
</Heading>
</View>
) : null}
{isSubscribed ? (
<View
style={{
backgroundColor: colors.primary.accent,
borderRadius: defaultBorderRadius,
paddingHorizontal: 6,
alignItems: "center",
justifyContent: "center"
}}
>
<Heading color={colors.static.white} size={AppFontSize.xs}>
{strings.currentPlan()}
</Heading>
</View>
) : null}
</View>
<Paragraph size={AppFontSize.md}>
{isAnnual || is5YearProduct
? `${props.pricingPlans.getPrice(
product as RNIap.Subscription,
props.pricingPlans.hasTrialOffer(
undefined,
(product as RNIap.Subscription)?.productId
)
? 1
: 0,
isAnnual
)}/${strings.month()}`
: null}
{!isAnnual && !is5YearProduct
? `${props.pricingPlans.getStandardPrice(
product as RNIap.Subscription
)}/${strings.month()}`
: null}
</Paragraph>
</View>
</TouchableOpacity>
);
};

Some files were not shown because too many files have changed in this diff Show More