Compare commits

...

419 Commits

Author SHA1 Message Date
Ammar Ahmed
5bd032e480 mobile: release v3.3.5 2025-10-20 13:53:14 +05:00
Abdullah Atta
c1bd4cfb01 web: bump version to 3.3.2 2025-10-20 13:01:57 +05:00
Abdullah Atta
0b943d8ecd editor: fix bullet list created as sub list of outline list 2025-10-20 12:52:57 +05:00
Ammar Ahmed
75e22a591e Merge pull request #8802 from streetwriters/fix-reset-root-state
Fix reset root state
2025-10-20 12:44:13 +05:00
Ammar Ahmed
66498e12ce mobile: fix multiple timers set on reset root state causing jank 2025-10-20 12:40:20 +05:00
Ammar Ahmed
d5fefd2f10 Merge pull request #8801 from streetwriters/fix-collapsible-headings
Fix collapsible headings on mobile
2025-10-20 12:12:20 +05:00
Ammar Ahmed
e2556d3228 mobile: fix collapsible heading 2025-10-20 12:09:22 +05:00
Abdullah Atta
2eb38d236e web: allow discounts in overlay checkout 2025-10-20 11:00:26 +05:00
Abdullah Atta
ea52893be1 Merge branch 'master' into beta 2025-10-20 10:43:07 +05:00
Abdullah Atta
eef63d2028 Merge pull request #8799 from streetwriters/fix/entities-in-markdown-import
Fix html entities in markdown import
2025-10-20 10:37:32 +05:00
Abdullah Atta
14e0112e17 Merge pull request #8798 from streetwriters/fix/handle-attachment-errors-during-export
Handle attachment errors on export
2025-10-20 10:26:56 +05:00
Abdullah Atta
8ec9034fb2 Merge pull request #8797 from streetwriters/fix/disable-in-app-notices
Disable in-app noticies (temporarily)
2025-10-20 10:26:35 +05:00
Abdullah Atta
dc5973d75d editor: update package-lock file 2025-10-20 10:24:23 +05:00
Abdullah Atta
12126b2f9f web: fix html entities in markdown import 2025-10-20 10:17:59 +05:00
Abdullah Atta
9fa256be0e web: handle attachment errors on export 2025-10-20 08:58:22 +05:00
Abdullah Atta
c103328098 web: disable in-app noticies (temporarily) 2025-10-20 08:52:22 +05:00
Ammar Ahmed
ea2022bbe4 Merge branch 'mobile-release-v3.3.4' into beta 2025-10-20 08:39:56 +05:00
Ammar Ahmed
d7ced57319 Merge pull request #8796 from streetwriters/mobile-release-v3.3.4
Mobile release v3.3.4
2025-10-20 08:38:33 +05:00
Ammar Ahmed
2cc200dc2b mobile: fix paths for fonts 2025-10-18 13:35:23 +05:00
Ammar Ahmed
baa74d0353 mobile: fix build error due to tsgo usage 2025-10-18 12:56:40 +05:00
Ammar Ahmed
72b906da14 mobile: use currency symbol when possible 2025-10-18 12:15:30 +05:00
Ammar Ahmed
6db8c2fac2 mobile: release v3.3.4 2025-10-18 12:02:00 +05:00
Ammar Ahmed
09d5c0a65e mobile: fix sharing files from other apps to Notesnook 2025-10-18 12:01:19 +05:00
Ammar Ahmed
0b6b6a02cc editor: fix crash when adding new row in column 2025-10-18 09:04:06 +05:00
Abdullah Atta
f49c673e8e editor: fix crash when adding new row in table 2025-10-18 09:00:36 +05:00
Ammar Ahmed
64a8e9a075 mobile: set github release=false 2025-10-18 09:00:12 +05:00
Ammar Ahmed
e998b07ca5 mobile: fix strings 2025-10-18 08:57:54 +05:00
Ammar Ahmed
c189279030 mobile: fix sheet insets on android 16+ 2025-10-18 08:57:54 +05:00
Ammar Ahmed
12ddd6f958 mobile: add release type in github issue template 2025-10-18 08:57:54 +05:00
Ammar Ahmed
da06afda8a mobile: fix pricing on github release 2025-10-18 08:57:54 +05:00
Ammar Ahmed
bf808b7a2e mobile: fix editor toolbar height 2025-10-18 08:57:54 +05:00
Ammar Ahmed
13476a3f50 editor: fix checklist style 2025-10-18 08:56:04 +05:00
Ammar Ahmed
cfdf77a7b5 Merge branch 'fix-plans-showing-price' into beta 2025-10-17 09:59:38 +05:00
Ammar Ahmed
070c90362a Merge pull request #8782 from streetwriters/fix-plans-showing-price
Fix prices on github release and open checkout link in browser
2025-10-17 09:58:29 +05:00
Ammar Ahmed
c8d9858a0c Merge branch 'mobile-release-3.3.3' into beta 2025-10-17 09:11:28 +05:00
Ammar Ahmed
2685f60409 Merge pull request #8781 from streetwriters/mobile-release-3.3.3
mobile: release v3.3.3
2025-10-17 09:10:51 +05:00
Abdullah Atta
8d3bb5185c Merge branch 'master' into beta 2025-10-17 08:51:19 +05:00
Abdullah Atta
1f748a4026 global: introducing Notesnook Circle (#8779)
* core: add api for getting Notesnook Circle partners

* web: introduce notesnook circle

* core: export CirclePartner type

* mobile: add notesnook-circle

* web: use strings from intl for notesnook circle

* common: add notesnook circle to features

* web: fix local ips set for hosts

* mobile: fix db hosts

---------

Co-authored-by: Ammar Ahmed <ammarahmed6506@gmail.com>
2025-10-17 08:49:32 +05:00
Sudlon
b27f064ad4 editor: Add keybindings for highlighting the currently selected text (#8577)
* editor: Add keybindings for highlighting the currently selected text

Signed-off-by: Sudlon <sudlondon@outlook.com>

* editor: Change keybinding command to TipTap "syntax"

Signed-off-by: Sudlon <sudlondon@outlook.com>

* editor: Remove SHIFT-keybinding (leaving ALT-keybinding) for highlighting currently selected text

Signed-off-by: Sudlon sudlondon@outlook.com

* editor: Remove left-over references to the double keybinding for highlighting currently selected text

Signed-off-by: Sudlon sudlondon@outlook.com

---------

Signed-off-by: Sudlon <sudlondon@outlook.com>
Signed-off-by: Sudlon sudlondon@outlook.com
Co-authored-by: sudlon <sudlondon@outlook.com>
2025-10-16 16:59:37 +05:00
01zulfi
27f781cc63 editor: improve codeblock scrollbar styling (#8548)
* editor: improve codeblock scrollbar styling
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* editor: fix overflowed codeblock's bg color
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-16 16:50:35 +05:00
Abdullah Atta
1427958eca Merge pull request #8457 from 01zulfi/editor/tasklist-checklist-styling
editor: improve checklist styling
2025-10-16 15:01:24 +05:00
Abdullah Atta
436e09c255 editor: fix text direction not working in check lists 2025-10-16 14:50:13 +05:00
Abdullah Atta
cc9b2dd5b0 editor: add back line through for checked items 2025-10-16 14:45:40 +05:00
Abdullah Atta
0ba25419f3 editor: use single li element for simple checklists 2025-10-16 14:43:51 +05:00
Abdullah Atta
9238de4cb4 Merge pull request #8772 from 01zulfi/editor/fix-images-in-outline-lists
editor: fix inserting images in outline lists
2025-10-16 11:48:34 +05:00
01zulfi
2d5fe1058c editor: add test for code block in outline list
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-16 10:46:31 +05:00
01zulfi
b01d1fc87e editor: improve checklist styling
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-16 09:28:02 +05:00
Abdullah Atta
9f4c0a4c9a editor: move outline list item indicator to li from first child 2025-10-16 09:23:19 +05:00
Abdullah Atta
9eebda86c2 editor: fix enter/tab not working in block nodes inside outline list item 2025-10-16 09:22:49 +05:00
Abdullah Atta
d43e856fea editor: add support for all block types in outline list item 2025-10-16 09:22:15 +05:00
Abdullah Atta
7af1596038 editor: remove custom inline image -> block image migration 2025-10-16 09:21:45 +05:00
01zulfi
652f6eab0b editor: fix inserting images in outline lists
* inserting images in outline list was splitting the outline list into two, this commit fixes it

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-15 11:14:24 +05:00
Abdullah Atta
fdaddfdb92 Merge pull request #8767 from streetwriters/chore/update-electron-to-37 2025-10-13 14:12:34 +05:00
Abdullah Atta
9a1adf2065 Merge pull request #8767 from streetwriters/chore/update-electron-to-37
Update electron to v37
2025-10-13 14:12:01 +05:00
Abdullah Atta
f10b258f58 desktop: update electron to v37 2025-10-13 14:10:52 +05:00
01zulfi
7f558cbd41 core: handle inbox item sync & decryption (#8733)
* core: handle inbox item sync & decryption
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: minor refactors in handling inbox item sync
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: use inbox item salt
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: check inbox item version
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-13 14:02:32 +05:00
Ammar Ahmed
641eb28ce7 mobile: open web purchase in browser. 2025-10-13 11:33:48 +05:00
Ammar Ahmed
2c59a5f6f9 mobile: fix plan price not showing properly on gh release 2025-10-13 09:30:48 +05:00
Abdullah Atta
712f02e61e Merge branch 'master' into beta 2025-10-10 18:13:46 +05:00
Abdullah Atta
e5ad405a23 Merge pull request #8754 from streetwriters/release/3.3.1
Release v3.3.1
2025-10-10 18:12:40 +05:00
Abdullah Atta
cd1b78d8df ci: fix desktop builds 2025-10-10 17:53:41 +05:00
Abdullah Atta
3b1ca46945 web: fix desktop tests 2025-10-10 16:23:11 +05:00
Abdullah Atta
96c1636f88 web: bump version to 3.3.1 2025-10-10 15:54:29 +05:00
Abdullah Atta
a10f949856 Merge branch 'master' into beta 2025-10-10 15:53:32 +05:00
Abdullah Atta
9ff4757ed6 Merge pull request #8753 from streetwriters/fix/make-table-with-adjustable
Make table width adjustable
2025-10-10 15:52:26 +05:00
Abdullah Atta
17c1e77f54 editor: make table width adjustable 2025-10-10 15:47:05 +05:00
Abdullah Atta
187a2a7262 Merge pull request #8735 from 01zulfi/editor/restrict-pasting-premium-features
editor: restrict pasting premium features
2025-10-10 15:38:11 +05:00
Abdullah Atta
4621230549 Merge pull request #8751 from streetwriters/fix/discount-price-on-checkout
Fix discounted price in checkout
2025-10-10 15:33:42 +05:00
Abdullah Atta
d7cb3317a2 web: fix discounted price in checkout 2025-10-10 15:09:41 +05:00
01zulfi
6d81f26864 editor: restrict pasting premium features
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-08 13:31:24 +05:00
01zulfi
5219814b5a Merge pull request #8557 from 01zulfi/editor/refactor-collapsible-headings
editor: make headings collapsible
2025-10-08 13:26:37 +05:00
Abdullah Atta
bef9fd0fd0 Merge branch 'master' into beta 2025-10-08 13:25:42 +05:00
Abdullah Atta
2341293d92 Merge pull request #8734 from 01zulfi/common/fix-paragraph-keybinding
common: fix insert paragraph keybinding
2025-10-08 13:20:32 +05:00
Abdullah Atta
215387f8a9 Merge pull request #8731 from streetwriters/chore/get-rid-of-nx
Get rid of nx
2025-10-08 13:20:00 +05:00
01zulfi
534b8c5ad0 common: fix insert paragraph keybinding
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-08 11:57:42 +05:00
Abdullah Atta
b7b2ead9eb setup(tx): minor refactor 2025-10-08 10:57:58 +05:00
Abdullah Atta
b3e34d68e1 setup(tx): add support for passing flags down to commands 2025-10-08 10:55:52 +05:00
Abdullah Atta
39079612aa docs: remove mention of Nx from README 2025-10-08 10:29:18 +05:00
Abdullah Atta
be1af3111f setup: add support for passing args down to command 2025-10-08 10:28:02 +05:00
Abdullah Atta
8dcff49797 core: fix flaky note history tests 2025-10-08 10:27:43 +05:00
Abdullah Atta
687fb4e105 setup: remove editor-mobile from build all exclusion filter 2025-10-08 09:57:40 +05:00
Abdullah Atta
25ae780ad8 ci: get rid of NX_CLOUD_ACCESS_TOKEN 2025-10-08 09:49:05 +05:00
Abdullah Atta
b695367e6c global: add dotenv package 2025-10-08 09:34:59 +05:00
Abdullah Atta
bb274a84fe ci: use node v22 2025-10-08 08:56:20 +05:00
Abdullah Atta
364dccbf7a global: get rid of nx 2025-10-08 08:42:00 +05:00
Ammar Ahmed
105fb0fc69 mobile: release v3.3.3 2025-10-06 13:13:42 +05:00
Ammar Ahmed
89c408981c Merge branch 'master' into beta 2025-10-06 12:42:59 +05:00
Ammar Ahmed
110534edec Merge pull request #8714 from streetwriters/fix-mobile-client-selfhosted
Fix mobile client selfhosted
2025-10-06 12:40:26 +05:00
Ammar Ahmed
00e2190b2d Merge pull request #8713 from streetwriters/mobile-fix-legacy-pro-users
mobile: add support for LEGACY_PRO subscription plan name
2025-10-06 12:40:10 +05:00
Ammar Ahmed
e5c3fc667e Merge pull request #8712 from streetwriters/mobile-refactor-e2e-tests
mobile: refactor e2e tests
2025-10-06 12:39:57 +05:00
Ammar Ahmed
9aef8194f9 mobile: refactor e2e tests
Uses a builder pattern for writing tests to reduce code and simplify writing tests
2025-10-06 12:11:02 +05:00
Abdullah Atta
f76e0b7931 Merge pull request #8711 from 01zulfi/web/ignore-autobackup-notice-when-ignored
web: don't show auto-backup notice again after dismissal
2025-10-06 11:39:35 +05:00
Ammar Ahmed
0457e4417f mobile: minor cleanup 2025-10-06 11:10:19 +05:00
Ammar Ahmed
dae8b5c542 mobile: hide pricing flows on self hosted clients 2025-10-06 10:58:39 +05:00
01zulfi
60107dd77f web: don't show auto-backup notice again after dismissal
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-06 10:24:46 +05:00
Ammar Ahmed
4f3e19aaa2 mobile: add support for LEGACY_PRO subscription plan name 2025-10-06 09:55:28 +05:00
Ammar Ahmed
8c621f9f47 mobile: add null checks 2025-10-06 09:51:16 +05:00
Ammar Ahmed
74544efd62 mobile: update version to v3.3.2 2025-10-06 09:51:16 +05:00
Ammar Ahmed
c30ccbb68d mobile: fix crash on entering settings 2025-10-06 09:51:16 +05:00
Ammar Ahmed
c77050eccc Merge pull request #8710 from streetwriters/mobile-fix-3.3.1
mobile: release v3.3.2
2025-10-06 09:44:45 +05:00
Ammar Ahmed
f40ccdae41 mobile: add null checks 2025-10-05 01:11:09 +05:00
Ammar Ahmed
0a0aa09eea mobile: update version to v3.3.2 2025-10-05 01:02:56 +05:00
Ammar Ahmed
e2c55fb758 mobile: fix crash on entering settings 2025-10-05 00:58:02 +05:00
Ammar Ahmed
12e3335d52 mobile: release v3.3.1 2025-10-04 23:57:09 +05:00
Ammar Ahmed
f29439539d mobile: misc bug fixes 2025-10-04 23:57:09 +05:00
Ammar Ahmed
13c9fdba52 mobile: update monograph publish sheet 2025-10-04 23:57:09 +05:00
Ammar Ahmed
2cb087972c Merge pull request #8638 from streetwriters/mobile-release-v3.3.1
release v3.3.1 on mobile
2025-10-04 23:56:33 +05:00
Ammar Ahmed
41a32ede15 mobile: release v3.3.1 2025-10-04 23:27:30 +05:00
Abdullah Atta
95f52fba51 Merge pull request #8633 from streetwriters/release/v3.3 2025-10-04 22:08:21 +05:00
Abdullah Atta
e2159b7614 Merge pull request #8633 from streetwriters/release/v3.3
Release v3.3
2025-10-04 22:07:18 +05:00
Ammar Ahmed
ee33875edf mobile: misc bug fixes 2025-10-04 12:54:51 +05:00
Ammar Ahmed
28681fb1b0 mobile: update monograph publish sheet 2025-10-04 12:54:51 +05:00
Abdullah Atta
33194dc9f0 setup: show script logs only in verbose mode 2025-10-04 10:37:51 +05:00
Abdullah Atta
7509766df2 desktop: fix sqlite extensions can't be found in dev 2025-10-04 10:22:11 +05:00
Abdullah Atta
30a02a4977 core: fix type errors 2025-10-04 10:21:38 +05:00
Abdullah Atta
f67f4138c3 core: minor fixes to monographs 2025-10-04 10:18:38 +05:00
Abdullah Atta
49d51da1bd web: fix toggles not working in publish view 2025-10-04 10:18:22 +05:00
Abdullah Atta
17bfe4e075 desktop: handle errors thrown during backup creation 2025-10-04 09:31:15 +05:00
Abdullah Atta
d5905a4e5f desktop: update electron to v36 2025-10-04 09:28:55 +05:00
Abdullah Atta
e1b46d33f4 desktop: enable asar 2025-10-04 09:20:10 +05:00
Ammar Ahmed
ce7c003c54 mobile: fix bottom padding 2025-10-04 08:37:07 +05:00
Abdullah Atta
7f473ce7c8 ci: use custom task executor instead of nx 2025-10-04 08:27:09 +05:00
Abdullah Atta
c239f490b0 setup: add custom task runner as an alternative to nx 2025-10-04 00:09:23 +05:00
Abdullah Atta
1ad1915fcc config: cache {projectRoot}/locales 2025-10-03 21:42:11 +05:00
Abdullah Atta
14029de484 misc: update nx 2025-10-03 18:47:15 +05:00
Abdullah Atta
5d07db366b desktop: add logs in build script for debugging 2025-10-03 18:40:47 +05:00
Abdullah Atta
9e265dbecf web: add new version highlights 2025-10-03 18:38:08 +05:00
Abdullah Atta
717bce36e6 desktop: regenerate lockfile 2025-10-03 18:30:39 +05:00
Abdullah Atta
17831cdd8c ci: install sqlite3-fts5-html for all platforms 2025-10-03 18:13:12 +05:00
Ammar Ahmed
02e37c6ed4 mobile: prepare release v3.3.0 2025-10-03 14:39:04 +05:00
Abdullah Atta
16b52e9633 web: update export snapshots 2025-10-03 14:12:10 +05:00
Abdullah Atta
77eee945ca web: bump version to 3.3.0 2025-10-03 14:03:14 +05:00
Abdullah Atta
f21f235d37 core: remove pricing tests 2025-10-03 13:58:22 +05:00
Abdullah Atta
2949fcde38 core: fix e2e tests 2025-10-03 13:54:17 +05:00
Abdullah Atta
6ae1674de5 web: fix tests 2025-10-03 11:59:53 +05:00
Abdullah Atta
2bd74e043a core: fix note history cleanup tests 2025-10-03 09:05:52 +05:00
Abdullah Atta
1a05acbe9a core: fix isProduction always returns false 2025-10-03 09:03:12 +05:00
Abdullah Atta
a0597e13fe Revert "Merge pull request #8493 from 01zulfi/editor/collapsible-headings"
This reverts commit 7241dce7a6, reversing
changes made to ae5b422965.
2025-10-03 08:54:28 +05:00
Ammar Ahmed
d11d665395 mobile: do not allow changing plan if user subscribed to legacy pro sub 2025-10-03 08:41:11 +05:00
Abdullah Atta
ab2758ac79 intl: Revert "intl: clarify what happens to user data on plan switch"
This reverts commit 4377b8b340.
2025-10-02 14:45:13 +05:00
01zulfi
bb682a6f6e web: reset default notebook & tag feature if applicable (#8628)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-02 14:22:34 +05:00
Abdullah Atta
4377b8b340 intl: clarify what happens to user data on plan switch 2025-10-02 14:21:12 +05:00
Abdullah Atta
eb0c9b823b common: File size -> Maximum file size 2025-10-02 13:38:18 +05:00
Abdullah Atta
a595b12431 common: rerun feature available hooks on user subscription update 2025-10-02 11:26:16 +05:00
Abdullah Atta
ebd9b45cac web: fix features reset on subscription change 2025-10-02 11:17:08 +05:00
Abdullah Atta
5ae024f20a Merge branch 'master' into beta 2025-10-02 10:40:56 +05:00
Abdullah Atta
9ae93dec4c Merge pull request #8432 from 01zulfi/docs/standard-notes-import
docs: add standard notes import instructions
2025-10-02 10:35:02 +05:00
Abdullah Atta
93ecc9b1ac Merge pull request #8587 from 01zulfi/web/fix-status-item
web: fix update & vault status items vertical alignment
2025-10-02 10:33:01 +05:00
Ammar Ahmed
28455f8b31 mobile: update icons in settings 2025-10-02 09:43:46 +05:00
Abdullah Atta
b49be89b06 Merge pull request #8570 from 01zulfi/editor/support-math-paste
editor: support pasting math
2025-10-02 09:30:51 +05:00
Abdullah Atta
23e0748137 Merge pull request #8581 from streetwriters/fix/error-not-shown-gift-card
Fix error not shown on redeem code failure
2025-10-02 09:30:14 +05:00
Abdullah Atta
2f6d222cca Merge pull request #8603 from 01zulfi/core/fix-leading-spaces-export
core: fix leading spaces in txt export && remove unneeded space at start of txt
2025-10-02 09:29:23 +05:00
Abdullah Atta
ba1cfe05c4 Merge pull request #8621 from 01zulfi/core/fix-tag-title-sorting
core: fix tag sort by title
2025-10-02 09:29:07 +05:00
Abdullah Atta
6f80334296 core: fix tests 2025-10-02 09:24:50 +05:00
Ammar Ahmed
86da8a3620 mobile: add limit on 2fa sms setup 2025-10-02 09:09:20 +05:00
Abdullah Atta
11702c9209 Merge pull request #8615 from streetwriters/fix/web-app-not-working-on-mobile
Fix web app not working on mobile devices
2025-10-02 08:38:05 +05:00
Abdullah Atta
240d9b533f web: reset restricted features on app startup 2025-10-02 08:37:17 +05:00
01zulfi
8dc12f01b9 web: enforce sms2FA feature limit (#8627)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-01 14:58:42 +05:00
Ammar Ahmed
3b0dce6c6f mobile: fix crash 2025-10-01 13:42:59 +05:00
Ammar Ahmed
901dc4fe6d mobile: show discount on web plan offer 2025-10-01 13:42:59 +05:00
01zulfi
61748f40b6 web: fix text in upgrade dialog (#8623)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-10-01 11:45:30 +05:00
Abdullah Atta
871824f857 common: fix off by one error when checking limits 2025-10-01 10:34:11 +05:00
Ammar Ahmed
032c321568 mobile: fix announcements ui 2025-10-01 09:51:03 +05:00
Abdullah Atta
4883f02b62 web: fix subscription settings not updating on downgrade 2025-09-30 14:03:55 +05:00
Abdullah Atta
b43a8cbafe web: improve plan change dialog to show full plan comparison 2025-09-30 11:53:45 +05:00
Abdullah Atta
11c1fe3853 web: refresh user on subscription change 2025-09-30 11:53:45 +05:00
Ammar Ahmed
4a4576ebd5 mobile: allow changing plan if not subscribed anymore 2025-09-30 11:25:36 +05:00
Abdullah Atta
0b5128b13b web: fix after trial period pricing ui 2025-09-30 10:21:37 +05:00
Abdullah Atta
d1dbb85459 web: prevent changing plans if user subscription from iOS/Android 2025-09-30 10:17:57 +05:00
Abdullah Atta
ae1005d5f9 web: show provider info in subscription settings 2025-09-30 10:08:59 +05:00
Abdullah Atta
11e6074e21 web: simplify change plan dialog 2025-09-30 09:45:18 +05:00
Abdullah Atta
ea9aa2a136 web: navigate to /notes on subscription update in checkout view 2025-09-30 09:45:18 +05:00
Ammar Ahmed
203fe5bee0 mobile: update sheet paywall 2025-09-30 08:56:37 +05:00
Ammar Ahmed
4262641567 mobile: add subscription paused case 2025-09-30 08:55:53 +05:00
Ammar Ahmed
c0f79be78c mobile: fix keyboard does not open with autoFocus 2025-09-30 08:55:28 +05:00
01zulfi
239e301042 core: fix tag sort by title
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-29 11:23:03 +05:00
Ammar Ahmed
23bb80bbd6 Merge branch 'fix-server-url-error' into beta 2025-09-29 10:05:08 +05:00
Abdullah Atta
d7232a4f1f Merge pull request #8576 from streetwriters/feature/new-pricing
New pricing
2025-09-29 10:02:43 +05:00
Abdullah Atta
2bfc1af8ff common: add monograph link & embeds and sms 2fa feature limits 2025-09-29 10:02:11 +05:00
Abdullah Atta
3ac6873be4 core: fix pause/resume legacy subscription 2025-09-29 10:02:11 +05:00
Abdullah Atta
ce487a108c web: enable full page mode in web clipper for all users 2025-09-29 10:02:11 +05:00
Abdullah Atta
827930e28f web: fix plan changing if subscription is trialing 2025-09-29 10:02:11 +05:00
Abdullah Atta
8a13cf01b4 web: fix trial info not shwon during checkout 2025-09-29 10:02:11 +05:00
Abdullah Atta
641c541305 web: fallback to created date if billed at date is not present 2025-09-29 10:02:11 +05:00
Abdullah Atta
9285bae0cc web: hide refund button if subscription is trialing 2025-09-29 10:02:10 +05:00
Abdullah Atta
954ac3154f web: only show billing history if provider is Paddle 2025-09-29 10:02:10 +05:00
Abdullah Atta
571f3242e2 web: show checkout page instead of dialog from plans page 2025-09-29 10:02:10 +05:00
Abdullah Atta
9052948b8a web: notesnook logo should be a link in checkout page 2025-09-29 10:02:10 +05:00
Ammar Ahmed
af91a6ee98 mobile: minor fixes 2025-09-29 10:02:10 +05:00
Ammar Ahmed
16633dbd05 mobile: fix welcome message sheet ui 2025-09-29 10:02:10 +05:00
Ammar Ahmed
8a506edd37 mobile: fix navigation when changing plan 2025-09-29 10:02:10 +05:00
Ammar Ahmed
fdbac8e608 mobile: paywall fixes 2025-09-29 10:02:10 +05:00
Ammar Ahmed
cdca90bd54 core: add googlePurchaseToken type 2025-09-29 10:02:10 +05:00
Ammar Ahmed
fb7d8b0893 mobile: allow users to change plan 2025-09-29 10:02:10 +05:00
Ammar Ahmed
a49bc92a13 mobile: pricing fixes 2025-09-29 10:02:10 +05:00
Abdullah Atta
58c1f5e3ca web: add support for education plan purchase 2025-09-29 10:02:10 +05:00
Ammar Ahmed
f1a0c1b447 mobile: minor refactor 2025-09-29 10:02:10 +05:00
Ammar Ahmed
d8534da4df mobile: fix gap between plan cards 2025-09-29 10:02:10 +05:00
Ammar Ahmed
28daa38d80 mobile: fix upgrade plan button not showing 2025-09-29 10:02:10 +05:00
Ammar Ahmed
5c275adda8 mobile: fix imports 2025-09-29 10:02:10 +05:00
Ammar Ahmed
26ecf2a7fd editor: fix build error 2025-09-29 10:02:10 +05:00
Abdullah Atta
e23c0f55b7 web: improve change plan ux 2025-09-29 10:02:10 +05:00
Ammar Ahmed
a6b03635c7 mobile: update vector icons patch 2025-09-29 10:02:10 +05:00
Abdullah Atta
6587541729 web: send email in custom data on purchase 2025-09-29 10:02:10 +05:00
Abdullah Atta
bc4bfa63f7 web: go back on overlay checkout closed 2025-09-29 10:02:10 +05:00
Abdullah Atta
bdda2d4987 web: add support for direct checkout 2025-09-29 10:02:10 +05:00
Ammar Ahmed
ab34073870 mobile: fix ui on ipad and tablets 2025-09-29 10:02:10 +05:00
Abdullah Atta
152a59fe26 web: improve attachment errors on upload/download 2025-09-29 10:02:10 +05:00
Ammar Ahmed
cf169eb6d6 mobile: fix inconsistent button sizes 2025-09-29 10:02:10 +05:00
Ammar Ahmed
16da396289 mobile: fix ui 2025-09-29 10:02:10 +05:00
Ammar Ahmed
7ad3fd4057 mobile: refactor 2025-09-29 10:02:10 +05:00
Ammar Ahmed
aecf4f1ee8 mobile: fix attachment uploading 2025-09-29 10:02:10 +05:00
Abdullah Atta
8f48fcd788 web: fix crash on opening checkout for new user 2025-09-29 10:02:10 +05:00
Ammar Ahmed
cf64fb3743 core: fix db.pricing.sku type 2025-09-29 10:02:10 +05:00
Ammar Ahmed
a21111368e mobile: fix pricing on gh release 2025-09-29 10:02:10 +05:00
Abdullah Atta
383b914c52 web: post success message on checkout complete on react native 2025-09-29 10:02:10 +05:00
Abdullah Atta
555255a923 web: minor fix 2025-09-29 10:02:10 +05:00
Abdullah Atta
cd5a2ab179 web: fix cannot read property ASK_EVERY_TIME of undefined 2025-09-29 10:02:10 +05:00
Ammar Ahmed
48903f924c mobile: cleanup 2025-09-29 10:02:10 +05:00
Ammar Ahmed
7d46dd4fb0 mobile: cleanup 2025-09-29 10:02:10 +05:00
Abdullah Atta
cfa9f11f44 web: replace usage of subscription.type to subscription.plan 2025-09-29 10:02:10 +05:00
Abdullah Atta
1377ac38db common: no need for isLegacyPro 2025-09-29 10:02:10 +05:00
Abdullah Atta
d88b30b538 core: get rid of subscription.type (all subs now use .plan) 2025-09-29 10:02:10 +05:00
Ammar Ahmed
cfe0cded15 mobile: minor improvements 2025-09-29 10:02:10 +05:00
Ammar Ahmed
322f6b7aae mobile: pricing fixes 2025-09-29 10:02:10 +05:00
Ammar Ahmed
62ae26c464 mobile: pricing fixes for ios 2025-09-29 10:02:10 +05:00
Abdullah Atta
c920c999eb web: fix subscription settings for legacy pro users 2025-09-29 10:02:10 +05:00
Abdullah Atta
288f601651 web: use trialExpiryDate for showing trial end date 2025-09-29 10:02:10 +05:00
Ammar Ahmed
d4f902f63d mobile: fix crash when user logged out 2025-09-29 10:02:10 +05:00
Ammar Ahmed
7dddebef2e mobile: pricing fixes 2025-09-29 10:02:10 +05:00
Abdullah Atta
dc1498d672 web: ask for reason when requesting refund 2025-09-29 10:02:10 +05:00
Abdullah Atta
eb36c1d7cb web: show billing history even if user is not subscribed 2025-09-29 10:02:09 +05:00
Abdullah Atta
29c119a945 web: fix Upgrade to Pro being shown in drop down 2025-09-29 10:02:09 +05:00
Abdullah Atta
860bc10747 web: fix cannot use IS_DEV before initialization error 2025-09-29 10:02:09 +05:00
Abdullah Atta
215ca23190 web: add support for changing plans 2025-09-29 10:02:09 +05:00
Abdullah Atta
4b9d9e4353 web: add comparison table & faqs to plans selection dialog 2025-09-29 10:02:09 +05:00
Ammar Ahmed
fd5d4e56ea mobile: route to login page from paywall 2025-09-29 10:02:09 +05:00
Ammar Ahmed
9801d117a1 mobile: minor fixes 2025-09-29 10:02:09 +05:00
Ammar Ahmed
3c4fb8cb53 mobile: localize 2025-09-29 10:02:09 +05:00
Ammar Ahmed
ae17b34be9 mobile: localize strings 2025-09-29 10:02:09 +05:00
Ammar Ahmed
1a544754ef mobile: handle web payments 2025-09-29 10:02:09 +05:00
Ammar Ahmed
1ddfd15ee9 mobile: render faqs 2025-09-29 10:02:09 +05:00
Ammar Ahmed
035126ab2f mobile: small fixes 2025-09-29 10:02:09 +05:00
Abdullah Atta
e157ae8bc8 web: add checkout faqs 2025-09-29 10:02:09 +05:00
Abdullah Atta
25b0503a25 web: show features usage details in subscription settings 2025-09-29 10:02:09 +05:00
Abdullah Atta
ef235dd0aa web: show plans on login & signup 2025-09-29 10:02:09 +05:00
Ammar Ahmed
699080048d mobile: fix login flow 2025-09-29 10:02:09 +05:00
Ammar Ahmed
3c9b315f78 mobile: updates 2025-09-29 10:02:09 +05:00
Ammar Ahmed
1464006531 mobile: fix unneeded sync calls 2025-09-29 10:02:09 +05:00
Abdullah Atta
6921fcb3ae web: fix checkout 2025-09-29 10:02:09 +05:00
Abdullah Atta
286f8dd7cc core: add endpoint for getting checkout url 2025-09-29 10:02:09 +05:00
Abdullah Atta
d3e3ef5986 core: add endpoints for getting all products for new plans 2025-09-29 10:02:09 +05:00
Ammar Ahmed
ea1ffc59b8 common: fix too many queries to db 2025-09-29 10:02:09 +05:00
Abdullah Atta
5524886868 common: add function to fetch features usage details 2025-09-29 10:02:09 +05:00
Abdullah Atta
68b63cf635 web: introduce new checkout dialog 2025-09-29 10:02:09 +05:00
Abdullah Atta
74de09e441 web: use checkFeature in multi-select 2025-09-29 10:02:09 +05:00
Abdullah Atta
b82e9598bd theme: add new tertiary button variant 2025-09-29 10:02:09 +05:00
Abdullah Atta
25889192c4 web: refactor toggles in setting store 2025-09-29 10:02:09 +05:00
Abdullah Atta
76eac69f94 web: remove limit checking from state stores 2025-09-29 10:02:09 +05:00
Abdullah Atta
97f8f1e3c3 web: use checkFeature 2025-09-29 10:02:09 +05:00
Abdullah Atta
8722b6bc68 web: add feature limit check when adding new tag from editor 2025-09-29 10:02:09 +05:00
Abdullah Atta
8e8d2a4083 web: refactor and fix feature limit checks on toolbar customization 2025-09-29 10:02:09 +05:00
Abdullah Atta
4caac77706 web: show upgrade dialog on triggering feature limit in editor 2025-09-29 10:02:09 +05:00
Abdullah Atta
0f73f266b2 web: fix customizableSidebar feature limit checks 2025-09-29 10:02:09 +05:00
Abdullah Atta
8c70abda04 web: add feature limit check when adding new tag 2025-09-29 10:02:09 +05:00
Abdullah Atta
6881a5f432 web: prefer to show upgrade dialog before opening new notebook/tag/color dialog 2025-09-29 10:02:09 +05:00
Abdullah Atta
22e9ae6bf8 web: use new api for adding limits on settings 2025-09-29 10:02:09 +05:00
Abdullah Atta
eb73de6d8d web: add support for adding feature limits on settings 2025-09-29 10:02:09 +05:00
Abdullah Atta
1af7f1bd7f web: show upgrade dialog from toast 2025-09-29 10:02:09 +05:00
Abdullah Atta
3a0273a12b web: add upgrade dialog 2025-09-29 10:02:09 +05:00
Abdullah Atta
05795ca052 web: add feature limit checks to tag & notebook functions 2025-09-29 10:02:09 +05:00
Abdullah Atta
a5746c56eb web: check for defaultNotebookAndTag limit in editor 2025-09-29 10:02:09 +05:00
Abdullah Atta
80f66ab0ba web: remove app lock on first unlock if not allowed on user plan 2025-09-29 10:02:09 +05:00
Abdullah Atta
44e62f0eb0 core: rebuild trash cache even if trash cleanup interval is set to never 2025-09-29 10:02:09 +05:00
Abdullah Atta
a6e107f61d ui: add support for showing premium icon on menu items 2025-09-29 10:02:09 +05:00
Abdullah Atta
31bb78bfca web: add onBeforeOpen hook to dialog manager 2025-09-29 10:02:09 +05:00
Abdullah Atta
8bb9308173 web: fix crash on saving note 2025-09-29 10:02:09 +05:00
Abdullah Atta
46d279ca3b common: fix use-is-feature-available hook types 2025-09-29 10:02:09 +05:00
Ammar Ahmed
0a5bdaaa7f mobile: add plan preview in settings 2025-09-29 10:02:09 +05:00
Ammar Ahmed
3480b8c5da mobile: add sync control and offline mode limits 2025-09-29 10:02:09 +05:00
Ammar Ahmed
32e1fb5f56 mobile: update plan comparison 2025-09-29 10:02:09 +05:00
Abdullah Atta
d94e8d04c8 common: add /mo to storage limit caption
also add limits for fullOfflineMode & syncControls
2025-09-29 10:02:09 +05:00
Abdullah Atta
26e2b380b3 common: add function for showing features as a table 2025-09-29 10:02:09 +05:00
Ammar Ahmed
06710ff416 mobile: pricing changes 2025-09-29 10:02:09 +05:00
Ammar Ahmed
32a7f866c3 mobile: add limits to UI 2025-09-29 10:02:09 +05:00
Abdullah Atta
86b650712e common: export getFeature 2025-09-29 10:02:09 +05:00
Ammar Ahmed
0178e455f9 mobile: update paywall sheet 2025-09-29 10:02:09 +05:00
Ammar Ahmed
39693966c1 mobile: add feature manager 2025-09-29 10:02:09 +05:00
Ammar Ahmed
dc08a39173 mobile: introduce plan limits 2025-09-29 10:02:09 +05:00
Ammar Ahmed
1be518510a mobile: update 2025-09-29 10:02:09 +05:00
Ammar Ahmed
e513081010 mobile: fix build 2025-09-29 10:02:09 +05:00
Ammar Ahmed
9bc8bddc12 common: fix esm imports 2025-09-29 10:02:09 +05:00
Abdullah Atta
986b99f62e web: show used/total storage on user profile 2025-09-29 10:02:08 +05:00
Abdullah Atta
16092a4185 core: add types for storage limits 2025-09-29 10:02:08 +05:00
Abdullah Atta
26eafcfee9 web: upload direct to s3 via proxy 2025-09-29 10:02:08 +05:00
Abdullah Atta
f20c1ef4f4 web: clicking on "auto backups disabled" notice should open settings 2025-09-29 10:02:08 +05:00
Abdullah Atta
3f076d90c8 common: make feature checking more type safe 2025-09-29 10:02:08 +05:00
Abdullah Atta
df70fee70e common: add storage to feature limits 2025-09-29 10:02:08 +05:00
Ammar Ahmed
4df9c7d9f7 mobile: fix intro flow 2025-09-29 10:02:08 +05:00
Abdullah Atta
59916b924c web: markdown shortcuts should be turned off by default 2025-09-29 10:02:08 +05:00
Abdullah Atta
aafce93117 web: limit max note versions 2025-09-29 10:02:08 +05:00
Abdullah Atta
97ef91abdb web: limit set as homepage 2025-09-29 10:02:08 +05:00
Abdullah Atta
b426d61c3a web: add limits to app lock 2025-09-29 10:02:08 +05:00
Abdullah Atta
f08c484d40 web: add limits to block-level note linking 2025-09-29 10:02:08 +05:00
Abdullah Atta
b03a4559c5 core: make max note versions configurable 2025-09-29 10:02:08 +05:00
Abdullah Atta
d2f491ed66 common: add feature limits for app lock and note versions 2025-09-29 10:02:08 +05:00
Abdullah Atta
1a2a1da086 web: add limit on notebook restoration 2025-09-29 10:02:08 +05:00
Abdullah Atta
872f4d7e91 core: remove usage of db.features 2025-09-29 10:02:08 +05:00
Abdullah Atta
bce063e1bd web: add feature limits in almost all places 2025-09-29 10:02:08 +05:00
Abdullah Atta
e6dc5ddfed common: add is feature available utility 2025-09-29 10:02:08 +05:00
Abdullah Atta
bcbcd516f6 mobile: initial pricing changes 2025-09-29 10:01:59 +05:00
Ammar Ahmed
24da82313e mobile: pricing changes 2025-09-29 10:00:29 +05:00
Abdullah Atta
5fe29af51a web: migrate to paddle billing + new plans 2025-09-29 10:00:29 +05:00
Abdullah Atta
fbe551eb29 core: add new subscription types 2025-09-29 10:00:29 +05:00
Abdullah Atta
c097c4d457 web: fix web app not working on mobile devices
i thought service workers would work on Android but unfortunately, they are
super flaky causing lots of issues. I might revisit it later.
2025-09-26 11:03:41 +05:00
Ammar Ahmed
a792a3d6e4 mobile: allow underscores in url validation 2025-09-25 12:26:48 +05:00
Ammar Ahmed
28f1f276ce Merge pull request #8611 from streetwriters/fix-server-url-error
mobile: allow underscores in url validation
2025-09-25 12:24:18 +05:00
01zulfi
95795a20c2 core: fix leading spaces in txt export && remove unneeded space at start of txt
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-24 09:35:26 +05:00
Ammar Ahmed
64b7b57b3e Merge branch 'mobile-release-3.2.12' into beta 2025-09-19 10:48:47 +05:00
01zulfi
cd85753252 web: fix update & vault status items vertical alignment
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-18 14:19:56 +05:00
Abdullah Atta
2b1ac822ed editor: fix images not rendering 2025-09-18 11:19:36 +05:00
Abdullah Atta
b3c3890443 editor: fix type errors 2025-09-18 11:19:24 +05:00
Abdullah Atta
99e642780e web: fix error not shown on redeem code failure 2025-09-17 10:59:54 +05:00
Abdullah Atta
47088a0a66 Merge branch 'master' into beta 2025-09-16 13:04:21 +05:00
Ammar Ahmed
3260695a09 Merge branch 'intl-fix-strings' into beta 2025-09-16 10:26:40 +05:00
Abdullah Atta
3a3434b021 Merge pull request #8562 from
streetwriters/fix/refresh-note-on-attachment-delete
2025-09-16 10:26:19 +05:00
Ammar Ahmed
aeb4c984eb Merge branch 'fix-ipad-split-view' into beta 2025-09-16 10:10:17 +05:00
Ammar Ahmed
e94a2b2aa1 Merge branch 'mobile-editor-ui-update' into beta 2025-09-16 10:09:49 +05:00
Ammar Ahmed
9344eadf34 Merge branch 'mobile-reminder-fixes' into beta 2025-09-16 10:06:58 +05:00
Ammar Ahmed
e40c42afc3 Merge branch 'mobile-fix-add-notebook-btn' into beta 2025-09-16 10:06:34 +05:00
Ammar Ahmed
178d7e7471 Merge branch 'mobile-fix-notebook-tree' into beta 2025-09-16 10:06:11 +05:00
Ammar Ahmed
48a3eee232 Merge branch 'mobile-fix-reset-navigation' into beta 2025-09-16 10:05:00 +05:00
Ammar Ahmed
84af1a9411 Merge branch 'mobile-fix-string-email' into beta 2025-09-16 10:04:05 +05:00
Abdullah Atta
295ab2c073 editor: add migration to convert inline images into block nodes 2025-09-16 09:21:09 +05:00
Abdullah Atta
7aa350a9d2 editor: remove image float 2025-09-16 09:21:09 +05:00
Abdullah Atta
ee76e43fca editor: fix warnings when expanding all props into ToolButton 2025-09-16 09:21:09 +05:00
01zulfi
0fa3598783 web: add manage inbox api tokens section (#8552)
* web: add manage inbox api tokens section
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: add view inbox api key option
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: don't generate inbox api key on client
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: use separate class for inbox api keys && don't set created date on client
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-16 01:15:03 +05:00
01zulfi
02960fb5a9 editor: support pasting math
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-15 15:41:16 +05:00
Abdullah Atta
01c629f612 Merge pull request #8550 from streetwriters/release/web/v3.2.4 2025-09-12 08:52:11 +05:00
Abdullah Atta
1be6e6b444 Merge pull request #8388 from streetwriters/fix/theme-issues-2 2025-09-12 08:28:13 +05:00
Abdullah Atta
38579c37e1 Merge pull request #8549 from 01zulfi/web/fix-toc-crash 2025-09-11 13:49:32 +05:00
01zulfi
251145ea74 global: add enable inbox setting to generate/discard inbox key pair (#8527)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-09 22:12:44 +05:00
Abdullah Atta
5e4972c1d0 web: bump version to v3.3.0-beta.1 2025-09-09 14:57:07 +05:00
Abdullah Atta
64bfc80a42 Merge pull request #8395 from 01zulfi/editor/image-resizing-dimensions 2025-09-09 14:52:30 +05:00
Abdullah Atta
7241dce7a6 Merge pull request #8493 from 01zulfi/editor/collapsible-headings
editor: make headings collapsible
2025-09-09 14:50:27 +05:00
01zulfi
8f25f9ba3e editor: extract evaluating click bounds for collapsible nodes into a util
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-09 14:39:32 +05:00
01zulfi
6ab58c68e1 editor: make headings collapsible
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-09 14:39:32 +05:00
01zulfi
ae5b422965 editor: if selected text is link, autofill url field in insert link popup
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-09 13:07:55 +05:00
Abdullah Atta
f9324d9da5 Merge pull request #8489 from 01zulfi/editor/improve-table-styling
editor: improve table styling
2025-09-09 13:06:10 +05:00
Abdullah Atta
2362902675 Merge pull request #8546 from streetwriters/release/monograph/1.2.5 2025-09-09 12:53:44 +05:00
Abdullah Atta
669f830dd7 Merge pull request #8545 from streetwriters/fix-monograph-report-not-working 2025-09-09 11:59:27 +05:00
Abdullah Atta
23924a34a3 editor: move selection to newly added row/column 2025-09-08 10:22:15 +05:00
Abdullah Atta
428f1e746a editor: make table 100% width 2025-09-08 10:21:45 +05:00
01zulfi
6eb1f2a4a7 editor: install & use simplebar for table scroll bar
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-02 13:02:55 +05:00
Abdullah Atta
5bd1999269 Merge pull request #8487 from 01zulfi/web/reminder-search-delete 2025-08-29 11:43:30 +05:00
Abdullah Atta
827ca2e427 Merge pull request #8509 from 01zulfi/editor/handle-empty-link-hrefs 2025-08-29 11:42:16 +05:00
01zulfi
1ae9e04e7a editor: improve table styling
* make tool buttons more prominent
* match scroll styling to rest of app
* add padding to cells
* add bg to header row

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-08-20 14:43:02 +05:00
01zulfi
693311a3db core: use RequestFetchV2 hub method (#8483)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-08-20 11:22:49 +05:00
Ammar Ahmed
35d08a6dfa Merge branch 'mobile-fix-open-note-notification' into beta 2025-08-20 08:49:59 +05:00
Ammar Ahmed
6bb5e44442 Merge branch 'mobile-fix-notebook-tag-sorting-filter' into beta 2025-08-20 08:49:36 +05:00
Ammar Ahmed
54a0d0fd0c Merge branch 'mobile-fix-back-navigation' into beta 2025-08-20 08:48:58 +05:00
Ammar Ahmed
dfeb01320d Merge branch 'fix-cache-clear-error' into beta 2025-08-20 08:48:20 +05:00
Ammar Ahmed
d33209afcc Merge branch 'fix-open-share-exported-notes-ios' into beta 2025-08-20 08:47:40 +05:00
Abdullah Atta
5530270b06 Merge pull request #8466 from 01zulfi/web/list-pane-max-size 2025-08-19 09:53:06 +05:00
01zulfi
c759408fdc core: add support for monographs sync 2025-08-18 13:16:12 +05:00
Abdullah Atta
30553ed0f1 Merge pull request #8389 from 01zulfi/common/update-keybindings 2025-08-13 13:31:34 +05:00
Abdullah Atta
b315e70e1e Merge pull request #8392 from 01zulfi/web/fix-open-settings-shortcut 2025-08-13 13:30:11 +05:00
Abdullah Atta
d6c6db68ba Merge pull request #8385 from Scott-will/master 2025-08-13 13:27:51 +05:00
01zulfi
0738b96161 docs: add missing instruction in desktop app dev setup
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-08-13 12:24:28 +05:00
01zulfi
6cca06fcbc docs: add standard notes import instructions
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-08-07 12:09:23 +05:00
Ammar Ahmed
50feebc0b0 mobile: fix fdroid build 2025-07-31 09:10:52 +05:00
Ammar Ahmed
0e869708f7 Merge branch 'mobile-fix-fdroid-build' into beta 2025-07-29 15:05:53 +05:00
Ammar Ahmed
efda6f86ae Merge branch 'mobile-release-3.2.10-fix' into beta 2025-07-26 12:48:44 +05:00
Ammar Ahmed
54008a0de2 Merge branch 'mobile-release-3.2.9' into beta 2025-07-26 11:58:43 +05:00
Ammar Ahmed
67a398f583 Merge branch 'mobile-editor-ux-improvement' into beta 2025-07-26 10:53:01 +05:00
Ammar Ahmed
e88c755ba8 Merge branch 'mobile-fix-edit-notebook' into beta 2025-07-25 11:01:20 +05:00
Abdullah Atta
26282352cb Merge pull request #8387 from 01zulfi/docs/keyboard-shortcuts-heading 2025-07-23 08:33:14 +05:00
Abdullah Atta
1c3a6ffbde Merge pull request #7655 from 01zulfi/global/keyboard 2025-07-22 14:22:22 +05:00
Abdullah Atta
acbac95818 Merge pull request #8384 from streetwriters/themebuilder/release/1.4.0 2025-07-22 09:08:23 +05:00
Abdullah Atta
6bf4300372 Merge pull request #8382 from 01zulfi/web/note-linking-dialog 2025-07-22 08:42:14 +05:00
Abdullah Atta
6f8f6d47f8 Merge pull request #8379 from streetwriters/docs/remove-onenote-sn-docs 2025-07-21 12:25:10 +05:00
Abdullah Atta
04a48979eb Merge pull request #8365 from 01zulfi/web/fix-nav-pane-on-small-screens 2025-07-21 12:24:17 +05:00
Abdullah Atta
ff3a8fa738 Merge pull request #8381 from
streetwriters/fix/email-should-not-be-required-session-expiry
2025-07-21 12:20:27 +05:00
Abdullah Atta
83b6c6de1c Merge pull request #8380 from streetwriters/ci/run-tests-on-pr-ref 2025-07-21 11:48:01 +05:00
Abdullah Atta
dbe8c1b9e6 Merge pull request #8378 from streetwriters/ci/migrate-to-pr-target 2025-07-21 11:29:01 +05:00
Abdullah Atta
c9e6f3e532 Merge pull request #8372 from streetwriters/fix/web-tests 2025-07-21 09:54:05 +05:00
Abdullah Atta
2f40905372 Merge pull request #8366 from Scott-will/master 2025-07-18 12:31:52 +05:00
Abdullah Atta
976199e5c6 Merge pull request #8364 from 01zulfi/editor/fix-readonly-links 2025-07-16 11:51:51 +05:00
01zulfi
449287e25a common: fix archive export (#8362)
* fix "_archive" folder generation for each archive note
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-16 11:07:20 +05:00
Ammar Ahmed
db2669d699 Merge branch 'fix-app-lauch-crash-pixel-device' into beta 2025-07-16 10:48:37 +05:00
Ammar Ahmed
5ae7855c7f Merge branch 'mobile-release-3.2.8' into beta 2025-07-16 09:38:58 +05:00
Ammar Ahmed
d8ed47ddb8 Merge branch 'mobile-fix-sidebar-items-update' into beta 2025-07-15 10:58:16 +05:00
Ammar Ahmed
39bfc41d33 Merge branch 'mobile-fix-android-nav-bar' into beta 2025-07-15 10:58:07 +05:00
Ammar Ahmed
68a8ac588f Merge branch 'mobile-fix-color-tag-long-press-crash' into beta 2025-07-15 10:58:01 +05:00
Ammar Ahmed
49d9ece15a Merge branch 'mobile-fix-crash-on-move-notebook' into beta 2025-07-15 10:57:51 +05:00
Abdullah Atta
f9dc539057 Merge pull request #8318 from 01zulfi/web/dont-close-note-after-attachment-delete
web: don't close note after deleting its attachment
2025-07-14 13:33:08 +05:00
Abdullah Atta
833b40bdc3 Merge pull request #8350 from 01zulfi/web/hide-spell-checker-languages-on-mac 2025-07-14 12:58:11 +05:00
01zulfi
0ca1d82861 web: don't close note after deleting its attachment
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-14 12:20:11 +05:00
Abdullah Atta
3dd25b9f83 Merge pull request #8348 from 01zulfi/web/fix-link-in-importer 2025-07-14 11:55:06 +05:00
Abdullah Atta
20cd5bef16 Merge pull request #8346 from 01zulfi/web/middle-click-new-note-button 2025-07-14 11:08:08 +05:00
Abdullah Atta
9b058bb17f Merge pull request #8345 from 01zulfi/web/fix-toolbar-bug 2025-07-14 11:07:49 +05:00
Ammar Ahmed
2325f4e31c Merge branch 'mobile-release-3.2.7' into beta 2025-07-12 14:29:51 +05:00
Ammar Ahmed
c0a3df0aa8 Merge branch 'mobile-fix-empty-note-bug' into beta 2025-07-11 13:57:12 +05:00
Abdullah Atta
ba6be6c84d web: bump version to v3.3.0-beta.0 2025-07-11 11:22:38 +05:00
Ammar Ahmed
b126b98eb0 Merge branch 'release-mobile-3.2.6' into beta 2025-07-11 11:16:05 +05:00
Abdullah Atta
f14e3d0f5e Merge pull request #8330 from 01zulfi/monograph/fix-api-host-on-client 2025-07-11 10:52:06 +05:00
Ammar Ahmed
a13878d4a8 Merge branch 'mobile-fix-crash-on-boot-android' into beta 2025-07-08 14:40:24 +05:00
Ammar Ahmed
bc8de87cda Merge branch 'mobile-fix-duplicate-body-notification' into beta 2025-07-08 12:25:52 +05:00
01zulfi
3ca0089ee8 web: add character, spaces, paragraphs count in status bar popup (#8311)
* web: add character, spaces, paragraphs count in status bar popup
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: refactor selected text stats logic
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: use PopupPresenter for note statistics popup
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: improve gettng text for counting characters
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-07 11:54:01 +05:00
Abdullah Atta
29c9476c16 web: fix recovery key not getting set during account recovery 2025-07-04 19:54:17 +05:00
01zulfi
9e72f51851 web: disable reveal in list option when focus mode active (#8297)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-04 19:49:34 +05:00
01zulfi
a349611a30 web: fix scroll to search result (#8290)
* web: (wip) fix scroll to search result
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* web: fix scroll to search result
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-03 12:16:25 +05:00
01zulfi
1cd21087c2 common: include archive in export (#8279)
* common: include archive in export
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

* core: include archive state in md-frontmatter export
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-01 10:25:57 +05:00
01zulfi
eecb8f9c05 web: show color name tooltip in note properties panel (#8280)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-01 10:25:57 +05:00
Abdullah Atta
e36fd28f47 core: add support for fields in search (#8261)
* core: add support for field based searching

* core: add more fields

* core: add `tagged`, `colored`, `in_notebook` fields
2025-07-01 10:25:57 +05:00
01zulfi
7ce44f4d84 web: support compact search results view (#8269)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-01 10:25:57 +05:00
01zulfi
6d8e4e8ebc web: fix note create option in quick open dialog (#8263)
* open new tab when query empty
* use editor-store methods for note creation

Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-01 10:25:57 +05:00
01zulfi
deb507859a core: don't generate headline title if title was set during note creation (#8266)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-01 10:25:57 +05:00
01zulfi
a165675b2a web: fix update status height (#8267)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-01 10:25:57 +05:00
448 changed files with 36933 additions and 111677 deletions

View File

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

View File

@@ -52,7 +52,6 @@ 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: npx nx test:e2e @notesnook/core
run: npm run tx @notesnook/core:test:e2e

View File

@@ -65,17 +65,13 @@ 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: npx nx build:desktop @notesnook/web
run: npm run tx @notesnook/web:build:desktop
- name: Generate desktop build (beta)
if: ${{ inputs.release-track == 'beta' }}
run: BETA=true npx nx build:desktop @notesnook/web
run: BETA=true npm run tx @notesnook/web:build:desktop
- name: Build desktop bundle
working-directory: ./apps/desktop
@@ -199,9 +195,9 @@ jobs:
CSC_LINK: ${{ secrets.mac_certs }}
CSC_KEY_PASSWORD: ${{ secrets.mac_certs_password }}
run: |
npx nx run release --project @notesnook/desktop -- --variant=mas
npm run tx @notesnook/desktop:release -- --variant=mas
cd apps/desktop
yarn electron-builder --config=electron-builder.config.js --mac mas --universal -p never
working-directory: ./apps/desktop
- name: Build zip and dmg
env:
@@ -212,13 +208,13 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.api_key_id }}
APPLE_API_ISSUER: ${{ secrets.api_key_issuer_id }}
run: |
npx nx run release --project @notesnook/desktop
npm run tx @notesnook/desktop:release
cd apps/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
@@ -271,8 +267,7 @@ jobs:
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
run: npm run tx @notesnook/desktop:release
- name: Build snap
if: inputs.publish-snap
@@ -339,8 +334,7 @@ jobs:
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
run: npm run tx @notesnook/desktop:release
# - name: Build snap
# if: inputs.publish-snap
@@ -402,10 +396,13 @@ 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: npx nx run release --project @notesnook/desktop
run: node scripts/execute.mjs @notesnook/desktop:release
- name: Publish
env:

View File

@@ -33,12 +33,8 @@ 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: npx nx build:desktop @notesnook/web
run: npm run tx @notesnook/web:build:desktop
- name: Archive build artifact
uses: actions/upload-artifact@v4
@@ -70,8 +66,7 @@ jobs:
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
run: npm run tx @notesnook/desktop:release
- name: Build app
run: |
@@ -114,8 +109,7 @@ jobs:
npm run bootstrap -- --scope=desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
run: npm run tx @notesnook/desktop:release
- name: Build app
run: |
@@ -164,8 +158,7 @@ jobs:
working-directory: ./apps/desktop
- name: Build Electron wrapper
run: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
run: npm run tx @notesnook/desktop:release
- name: Build app
run: |
@@ -211,11 +204,13 @@ 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: npx nx run release --project @notesnook/desktop
working-directory: ./apps/desktop
run: node scripts/execute.mjs @notesnook/desktop:release
- name: Build app
run: |

View File

@@ -38,12 +38,8 @@ 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: npx nx build @notesnook/editor
run: npm run tx @notesnook/editor:build
- name: Run all @notesnook/editor tests
run: npx nx test @notesnook/editor
run: npm run tx @notesnook/editor:test

View File

@@ -25,7 +25,7 @@ jobs:
npm run bootstrap -- --scope=mobile
- name: Build packages
run: npx nx run @notesnook/mobile:build
run: npm run tx @notesnook/mobile:build
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2.11

View File

@@ -39,7 +39,7 @@ jobs:
echo ::set-output name=app_version::$(cat package.json | jq -r .version)
- name: Generate build
run: npx nx build @notesnook/monograph
run: npm run tx @notesnook/monograph:build
# Setup Buildx
- name: Docker Setup Buildx

View File

@@ -20,7 +20,6 @@ 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,7 +20,6 @@ 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,10 +27,6 @@ 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,7 +30,6 @@ 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

@@ -49,10 +49,6 @@ 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

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ site
node_modules.backup
.nx
/*.patch
.taskcache

View File

@@ -27,7 +27,6 @@ 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.**

View File

@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { execSync } from "child_process";
import { cp, readFile, writeFile } from "fs/promises";
import { cp } from "fs/promises";
import { fileURLToPath } from "node:url";
import path, { join, resolve } from "path";
import { _electron as electron } from "playwright";
@@ -59,7 +59,8 @@ export async function buildAndLaunchApp(
});
const { app, page, configPath, userDataDir } = await launchApp(
executablePath,
productName
productName,
options?.version
);
const ctx: AppContext = {
app,
@@ -70,7 +71,8 @@ export async function buildAndLaunchApp(
relaunch: async () => {
const { app, page, configPath, userDataDir } = await launchApp(
executablePath,
productName
productName,
options?.version
);
ctx.app = app;
ctx.page = page;
@@ -81,7 +83,11 @@ export async function buildAndLaunchApp(
return ctx;
}
async function launchApp(executablePath: string, packageName: string) {
async function launchApp(
executablePath: string,
packageName: string,
version?: string
) {
const userDataDir = resolve(
__dirname,
"..",
@@ -99,7 +105,12 @@ async function launchApp(executablePath: string, packageName: string) {
APPIMAGE: "true"
}
: (process.env as Record<string, string>)),
CUSTOM_USER_DATA_DIR: userDataDir
CUSTOM_USER_DATA_DIR: userDataDir,
...(version
? {
CUSTOM_APP_VERSION: version
}
: {})
}
});
@@ -147,33 +158,18 @@ export async function buildApp(version?: string) {
}
}
async function copyBuild({
version,
outputDir
}: {
version?: string;
outputDir: string;
}) {
async function copyBuild({ outputDir }: { outputDir: string }) {
return process.platform === "win32"
? await makeBuildCopyWindows(outputDir, productName, version)
? await makeBuildCopyWindows(outputDir, productName)
: process.platform === "darwin"
? await makeBuildCopyMacOS(outputDir, productName, version)
: await makeBuildCopyLinux(outputDir, productName, version);
? await makeBuildCopyMacOS(outputDir, productName)
: await makeBuildCopyLinux(outputDir, productName);
}
async function makeBuildCopyLinux(
outputDir: string,
productName: string,
version?: string
) {
async function makeBuildCopyLinux(outputDir: string, productName: string) {
const platformDir =
process.arch === "arm64" ? "linux-arm64-unpacked" : "linux-unpacked";
const appDir = await makeBuildCopy(
outputDir,
platformDir,
"resources",
version
);
const appDir = await makeBuildCopy(outputDir, platformDir);
return resolve(
__dirname,
"..",
@@ -182,34 +178,16 @@ async function makeBuildCopyLinux(
);
}
async function makeBuildCopyWindows(
outputDir: string,
productName: string,
version?: string
) {
async function makeBuildCopyWindows(outputDir: string, productName: string) {
const platformDir =
process.arch === "arm64" ? "win-arm64-unpacked" : "win-unpacked";
const appDir = await makeBuildCopy(
outputDir,
platformDir,
"resources",
version
);
const appDir = await makeBuildCopy(outputDir, platformDir);
return resolve(__dirname, "..", appDir, `${productName}.exe`);
}
async function makeBuildCopyMacOS(
outputDir: string,
productName: string,
version?: string
) {
async function makeBuildCopyMacOS(outputDir: string, productName: string) {
const platformDir = process.arch === "arm64" ? "mac-arm64" : "mac";
const appDir = await makeBuildCopy(
outputDir,
platformDir,
join(`${productName}.app`, "Contents", "Resources"),
version
);
const appDir = await makeBuildCopy(outputDir, platformDir);
return resolve(
__dirname,
"..",
@@ -221,12 +199,7 @@ async function makeBuildCopyMacOS(
);
}
async function makeBuildCopy(
outputDir: string,
platformDir: string,
resourcesDir: string,
version?: string
) {
async function makeBuildCopy(outputDir: string, platformDir: string) {
const appDir = outputDir;
await cp(join(SOURCE_DIR, platformDir), outputDir, {
recursive: true,
@@ -236,14 +209,6 @@ async function makeBuildCopy(
force: true
});
const packageJsonPath = join(appDir, resourcesDir, "app", "package.json");
const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
if (version) {
packageJson.version = version;
await writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));
}
return appDir;
}

View File

@@ -47,7 +47,11 @@ module.exports = {
copyright: `Copyright © ${year} Streetwriters (Private) Limited`,
artifactName: "notesnook_${os}_${arch}.${ext}",
generateUpdatesFilesForAllChannels: true,
asar: false,
asar: true,
asarUnpack: [
"node_modules/sqlite-better-trigram-@(linux|darwin|windows)-${arch}/**/*",
"node_modules/sqlite3-fts5-html-@(linux|darwin|windows)-${arch}/**/*"
],
files: [
"!*.chunk.js.map",
"!*.chunk.js.LICENSE.txt",

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.2.4",
"version": "3.3.2",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
@@ -32,7 +32,7 @@
"@notesnook/ui": "file:../../packages/ui",
"@trpc/client": "10.45.2",
"@trpc/server": "10.45.2",
"better-sqlite3-multiple-ciphers": "^11.10.0",
"better-sqlite3-multiple-ciphers": "^12.4.1",
"electron-trpc": "0.7.1",
"electron-updater": "^6.6.2",
"icojs": "^0.19.5",
@@ -47,7 +47,7 @@
"@types/node": "22.15.3",
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.3",
"electron": "^34.0.0",
"electron": "^37.0.0",
"electron-builder": "^26.0.12",
"esbuild": "0.21.5",
"node-abi": "^4.5.0",

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
console.log("starting build...");
import path from "path";
import fs from "fs/promises";
import { existsSync } from "fs";
@@ -26,6 +27,7 @@ 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);
@@ -34,11 +36,22 @@ 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(
"npx nx build:desktop @notesnook/web",
"node scripts/execute.mjs @notesnook/web:build:desktop",
path.join(__dirname, "..", "..", "..")
);
}

View File

@@ -123,15 +123,21 @@ export class SQLite {
// fts5 API, we must wait decrypt the database before we can load
// the extensions.
if (!this.extensionsLoaded && (await this.isDatabaseReady())) {
const betterTrigram = require("sqlite-better-trigram");
const fts5Html = require("sqlite3-fts5-html");
betterTrigram.load(this.sqlite);
fts5Html.load(this.sqlite);
this.extensionsLoaded = true;
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[]
@@ -175,3 +181,34 @@ export class SQLite {
}
}
}
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

@@ -19,6 +19,12 @@ 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",

File diff suppressed because one or more lines are too long

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 } from "@notesnook/common";
import { database, getFeature, getFeatureLimit } from "@notesnook/common";
import { logger as dbLogger, ICompressor } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import {
@@ -26,7 +26,6 @@ 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";
@@ -34,11 +33,24 @@ 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",
@@ -46,6 +58,7 @@ 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") || {})
});
@@ -73,6 +86,10 @@ 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

@@ -74,21 +74,6 @@ 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({
@@ -97,8 +82,9 @@ export async function uploadFile(
})
.fetch(
"PUT",
uploadUrl,
url,
{
...headers,
"content-type": ""
},
RNFetchBlob.wrap(filePath)

View File

@@ -25,10 +25,17 @@ 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 = useMessageStore((state) => state.announcements);
const [announcements, remove] = useMessageStore((state) => [
state.announcements,
state.remove
]);
let announcement = announcements.length > 0 ? announcements[0] : null;
const selectionMode = useSelectionStore((state) => state.selectionMode);
@@ -57,6 +64,38 @@ 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,7 +25,6 @@ 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";
@@ -45,18 +44,6 @@ 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 (
@@ -64,7 +51,8 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
style={{
paddingHorizontal: DefaultAppStyles.GAP,
...getStyle(style),
flexDirection: inline ? "row" : "column"
flexDirection: inline ? "row" : "column",
gap: DefaultAppStyles.GAP_SMALL
}}
>
<SheetProvider context="premium_cta" />
@@ -99,12 +87,10 @@ 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,
marginLeft: 12
paddingHorizontal: 0
}}
textStyle={{
textDecorationLine: "underline"
@@ -119,7 +105,6 @@ 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
@@ -129,10 +114,8 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
opacity: 1
}}
onPress={() => onPress(item)}
width={250}
style={{
marginBottom: DefaultAppStyles.GAP_VERTICAL,
borderRadius: 100
width: "100%"
}}
/>
))}
@@ -149,7 +132,7 @@ export const Cta = ({ actions, style = {}, color, inline }) => {
height={30}
style={{
minWidth: "50%",
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL
width: "100%"
}}
textStyle={{
textDecorationLine: "underline"

View File

@@ -21,16 +21,17 @@ 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={{
marginHorizontal: 12,
paddingHorizontal: DefaultAppStyles.GAP,
...getStyle(style),
textAlign: inline ? "left" : style?.textAlign
}}
size={inline ? AppFontSize.sm : AppFontSize.md}
size={AppFontSize.sm}
>
{text}
</Paragraph>

View File

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

View File

@@ -27,10 +27,6 @@ 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={{
@@ -42,46 +38,21 @@ export const Title = ({ text, style = {}, inline }) => {
>
<Heading
style={{
marginHorizontal: DefaultAppStyles.GAP,
paddingHorizontal: 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={{
marginHorizontal: DefaultAppStyles.GAP,
paddingHorizontal: DefaultAppStyles.GAP,
...getStyle(style),
marginTop: style?.marginTop || DefaultAppStyles.GAP_VERTICAL
}}

View File

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

View File

@@ -19,9 +19,6 @@ 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
@@ -31,9 +28,7 @@ import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
import { sleep } from "../../utils/time";
import BaseDialog from "../dialog/base-dialog";
import { Toast } from "../toast";
import { Button } from "../ui/button";
import { IconButton } from "../ui/icon-button";
import { hideAuth, initialAuthMode } from "./common";
import { initialAuthMode } from "./common";
import { Login } from "./login";
import { Signup } from "./signup";
import { strings } from "@notesnook/intl";
@@ -51,7 +46,6 @@ const AuthModal = () => {
const [visible, setVisible] = useState(false);
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
const actionSheetRef = useRef();
const insets = useGlobalSafeAreaInsets();
useEffect(() => {
eSubscribeEvent(eOpenLoginDialog, open);
@@ -99,81 +93,18 @@ const AuthModal = () => {
centered={false}
enableSheetKeyboardHandler
>
<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>
{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)}
/>
)}
<Toast context="local" />
</BaseDialog>

View File

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

View File

@@ -0,0 +1,78 @@
/*
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,102 +17,29 @@ 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 { View } from "react-native";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import Navigation from "../../services/navigation";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
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 { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { DefaultAppStyles } from "../../utils/styles";
import { useSettingStore } from "../../stores/use-setting-store";
import { useThemeColors } from "@notesnook/theme";
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 (
<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");
setTimeout(() => {
Navigation.resetRootState();
}, 1000);
} 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 ? (
<SafeAreaView
style={{ flex: 1, backgroundColor: colors.primary.background }}
>
{currentAuthMode !== AuthMode.login &&
currentAuthMode !== AuthMode.welcomeLogin ? (
<Signup
changeMode={(mode) => setCurrentAuthMode(mode)}
trial={AuthMode.trialSignup === currentAuthMode}
@@ -126,7 +53,7 @@ const Auth = ({ navigation, route }) => {
)}
<Toast context="local" />
</View>
</SafeAreaView>
);
};

View File

@@ -19,17 +19,22 @@ 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 SheetProvider from "../sheet-provider";
import { Dialog } from "../dialog";
import { Progress } from "../sheets/progress";
import { Button } from "../ui/button";
import Input from "../ui/input";
@@ -37,9 +42,9 @@ 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 { DefaultAppStyles } from "../../utils/styles";
import { Dialog } from "../dialog";
import SettingsService from "../../services/settings";
const LoginSteps = {
emailAuth: 1,
@@ -50,6 +55,7 @@ const LoginSteps = {
export const Login = ({ changeMode }) => {
const { colors } = useThemeColors();
const [focused, setFocused] = useState(false);
const route = useRoute();
const {
step,
setStep,
@@ -62,15 +68,24 @@ export const Login = ({ changeMode }) => {
setError,
login
} = useLogin(async () => {
hideAuth();
eSendEvent(eUserLoggedIn, true);
await sleep(500);
Progress.present();
hideAuth();
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;
@@ -89,217 +104,227 @@ export const Login = ({ changeMode }) => {
return (
<>
<AuthHeader />
<ForgotPassword />
<Dialog context="two_factor_verify" />
<View
<KeyboardAwareScrollView
style={{
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
height: "100%",
alignSelf: "center"
width: "100%"
}}
contentContainerStyle={{
minHeight: "90%"
}}
nestedScrollEnabled
enableAutomaticScroll={false}
keyboardShouldPersistTaps="handled"
>
<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={{
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%",
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
alignSelf: "center",
paddingHorizontal: DefaultAppStyles.GAP
zIndex: 10,
width: "100%",
height: "100%",
alignSelf: "center"
}}
>
<Input
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
<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
}}
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
title={strings.forgotPassword()}
>
<View
style={{
flexDirection: "row"
}}
>
<View
style={{
alignSelf: "flex-end",
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL,
paddingHorizontal: 0
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
}}
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
style={{
marginTop: 25
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
}}
>
<Button
loading={loading}
onPress={() => {
if (loading) return;
login();
<Input
fwdRef={emailInputRef}
onChangeText={(value) => {
email.current = value;
}}
style={{
width: 250
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();
}
}}
type="accent"
title={!loading ? strings.continue() : null}
/>
{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"
/>
<>
<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"
/>
</>
)}
{!loading ? (
<TouchableOpacity
<View>
<Button
loading={loading}
onPress={() => {
if (loading) return;
changeMode(1);
login();
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
width: "100%"
}}
>
<Paragraph
size={AppFontSize.xs}
color={colors.secondary.paragraph}
type="accent"
title={!loading ? strings.continue() : null}
fontSize={AppFontSize.sm}
/>
{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"
/>
)}
{!loading ? (
<TouchableOpacity
onPress={() => {
if (loading) return;
changeMode(1);
}}
activeOpacity={0.8}
style={{
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
{strings.dontHaveAccount()}{" "}
<Paragraph
size={AppFontSize.xs}
style={{ color: colors.primary.accent }}
color={colors.secondary.paragraph}
>
{strings.signUp()}
{strings.dontHaveAccount()}{" "}
<Paragraph
size={AppFontSize.xs}
style={{ color: colors.primary.accent }}
>
{strings.signUp()}
</Paragraph>
</Paragraph>
</Paragraph>
</TouchableOpacity>
) : null}
</TouchableOpacity>
) : null}
</View>
</View>
</View>
</View>
</KeyboardAwareScrollView>
</>
);
};

View File

@@ -16,16 +16,12 @@ 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";
const path = require("path");
module.exports = {
entry: ["./dist/index.global.js"],
mode: "production",
output: {
filename: "clipper.bundle.js",
path: path.resolve(
__dirname,
"../../apps/mobile/native/ios/extension.bundle"
)
}
};
export const SignupContext = React.createContext<{
signup?: () => Promise<boolean>;
}>({
signup: undefined
});
export const useSignupContext = () => useContext(SignupContext);

View File

@@ -19,26 +19,35 @@ 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 PremiumService from "../../services/premium";
import SettingsService from "../../services/settings";
import Navigation from "../../services/navigation";
import { useUserStore } from "../../stores/use-user-store";
import { openLinkInBrowser } from "../../utils/functions";
import { AppFontSize } from "../../utils/size";
import { sleep } from "../../utils/time";
import { DefaultAppStyles } from "../../utils/styles";
import { Loading } from "../loading";
import { Button } from "../ui/button";
import Input from "../ui/input";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { hideAuth } from "./common";
import { DefaultAppStyles } from "../../utils/styles";
import { AuthHeader } from "./header";
import { SignupContext } from "./signup-context";
export const Signup = ({ changeMode, trial }) => {
const SignupSteps = {
signup: 0,
selectPlan: 1,
createAccount: 2
};
export const Signup = ({ changeMode, welcome }) => {
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
const { colors } = useThemeColors();
const email = useRef();
const emailInputRef = useRef();
@@ -52,6 +61,8 @@ export const Signup = ({ changeMode, trial }) => {
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({
@@ -70,23 +81,26 @@ export const Signup = ({ changeMode, trial }) => {
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();
hideAuth();
SettingsService.setProperty("encryptedBackup", true);
await sleep(300);
if (trial) {
PremiumService.sheet(null, null, true);
} else {
PremiumService.showVerifyEmailDialog();
if (!SettingsService.getProperty("serverUrls")) {
Navigation.navigate("PayWall", {
canGoBack: false,
state: route.params.state,
context: "signup"
});
}
return true;
} catch (e) {
setCurrentStep(SignupSteps.signup);
setLoading(false);
ToastManager.show({
heading: strings.signupFailed(),
@@ -94,214 +108,264 @@ export const Signup = ({ changeMode, trial }) => {
type: "error",
context: "local"
});
return false;
}
};
return (
<>
<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
<SignupContext.Provider
value={{
signup: signup
}}
>
{currentStep === SignupSteps.signup ? (
<>
<AuthHeader welcome={welcome} />
<KeyboardAwareScrollView
style={{
flexDirection: "row"
width: "100%"
}}
contentContainerStyle={{
minHeight: "90%"
}}
nestedScrollEnabled
enableAutomaticScroll={false}
keyboardShouldPersistTaps="handled"
>
<View
style={{
width: 100,
height: 5,
backgroundColor: colors.primary.accent,
borderRadius: 2,
marginRight: 7
borderRadius: DDS.isTab ? 5 : 0,
backgroundColor: colors.primary.background,
zIndex: 10,
width: "100%",
alignSelf: "center",
height: "100%"
}}
/>
<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: 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();
}}
/>
<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 }}
<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
}}
>
{strings.login()}
</Paragraph>
</Paragraph>
</TouchableOpacity>
</View>
</View>
</>
<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: 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..."
/>
</>
)}
</SignupContext.Provider>
);
};

View File

@@ -136,6 +136,11 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
backgroundColor: colors.primary.background,
paddingTop: 60
}}
onLayout={() => {
setTimeout(() => {
inputRef.current?.focus();
}, 500);
}}
>
<View
style={{
@@ -206,13 +211,15 @@ 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: 10,
letterSpacing: 7,
width: 250
}}
keyboardType={
@@ -220,8 +227,6 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo, onCancel }) => {
}
enablesReturnKeyAutomatically
containerStyle={{
borderWidth: 0,
width: undefined,
minWidth: "50%"
}}
wrapperStyle={{

View File

@@ -17,16 +17,17 @@ 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 { eSendEvent, ToastManager } from "../../services/event-manager";
import { ToastManager, eSendEvent } 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 TwoFactorVerification from "./two-factor";
import { strings } from "@notesnook/intl";
export const LoginSteps = {
emailAuth: 1,
@@ -34,15 +35,18 @@ export const LoginSteps = {
passwordAuth: 3
};
export const useLogin = (onFinishLogin, sessionExpired = false) => {
export const useLogin = (
onFinishLogin?: () => void,
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();
const password = useRef();
const emailInputRef = useRef();
const passwordInputRef = useRef();
const email = useRef<string>();
const password = useRef<string>();
const emailInputRef = useRef<TextInput>(null);
const passwordInputRef = useRef<TextInput>(null);
const validateInfo = () => {
if (
@@ -69,11 +73,15 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
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, callback) => {
async (mfa: any, callback: (success: boolean) => void) => {
try {
const success = await db.user.authenticateMultiFactorCode(
mfa.code,
@@ -111,10 +119,14 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
break;
}
case LoginSteps.passwordAuth: {
if (!email.current || !password.current) {
setLoading(false);
return;
}
await db.user.authenticatePassword(
email.current,
password.current,
null,
undefined,
sessionExpired
);
finishLogin();
@@ -123,11 +135,11 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
}
setLoading(false);
} catch (e) {
finishWithError(e);
finishWithError(e as Error);
}
};
const finishWithError = async (e) => {
const finishWithError = async (e: Error) => {
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
setLoading(false);
ToastManager.show({
@@ -146,7 +158,7 @@ export const useLogin = (onFinishLogin, sessionExpired = false) => {
clearMessage();
ToastManager.show({
heading: strings.loginSuccess(),
message: strings.loginSuccessDesc(),
message: strings.loginSuccessDesc(user.email),
type: "success",
context: "global"
});

View File

@@ -31,8 +31,6 @@ 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";
@@ -46,7 +44,6 @@ const DialogProvider = () => {
<AppLockPassword />
<LoadingDialog />
<Dialog context="global" />
<PremiumDialog colors={colors} />
<AuthModal colors={colors} />
<MergeConflicts />
<RecoveryKeySheet colors={colors} />
@@ -56,7 +53,6 @@ const DialogProvider = () => {
<VaultDialog colors={colors} />
<RateAppSheet />
<ImagePreview />
<Expiring />
<AnnouncementDialog />
<SessionExpired />
<PDFPreview />

View File

@@ -16,6 +16,9 @@ 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,
@@ -25,15 +28,13 @@ import {
View
} from "react-native";
import { Image as ImageType } from "react-native-image-crop-picker";
import { useThemeColors } from "@notesnook/theme";
import { presentSheet } from "../../../services/event-manager";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
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,
@@ -44,6 +45,7 @@ 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);
@@ -104,21 +106,42 @@ export default function AttachImage({
alignSelf: "center",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
alignItems: "center",
width: "100%"
width: "100%",
opacity: fullQualityImagesResult?.isAllowed ? 1 : 0.5
}}
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 ? colors.primary.accent : colors.primary.icon}
color={
compress && fullQualityImagesResult?.isAllowed
? 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

@@ -244,7 +244,7 @@ const PDFPreview = () => {
textAlign: "center",
marginRight: 4,
borderRadius: 3,
fontFamily: "OpenSans-Regular"
fontFamily: "Inter-Regular"
}}
selectTextOnFocus
keyboardType="decimal-pad"

View File

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

View File

@@ -20,21 +20,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { Linking, ScrollView, useWindowDimensions, View } from "react-native";
import { Linking, 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 { AuthMode } from "../auth/common";
import { DefaultAppStyles } from "../../utils/styles";
import { SafeAreaView } from "react-native-safe-area-context";
const Intro = () => {
const { colors } = useThemeColors();
const { width, height } = useWindowDimensions();
const { width } = useWindowDimensions();
const insets = useGlobalSafeAreaInsets();
const isTablet = width > 600;
@@ -115,75 +116,102 @@ const Intro = () => {
);
return (
<ScrollView
testID="notesnook.splashscreen"
<SafeAreaView
style={{
width: "100%",
flex: 1,
height: "100%",
backgroundColor: colors.primary.background
}}
>
<View
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
}
]}
testID="notesnook.splashscreen"
style={{
flex: 1
}}
>
<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
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>
</View>
<View
style={{
width: "100%",
width: isTablet ? "50%" : "100%",
justifyContent: "center",
minHeight: height * 0.3
gap: DefaultAppStyles.GAP_VERTICAL,
paddingHorizontal: isTablet ? 0 : DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
flexShrink: 1,
alignSelf: "center"
}}
>
<Button
width={250}
style={{
width: "100%"
}}
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>
</ScrollView>
</SafeAreaView>
);
};

View File

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

View File

@@ -30,6 +30,10 @@ 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"> {
@@ -59,6 +63,9 @@ 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();
@@ -125,6 +132,7 @@ 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);
@@ -158,15 +166,39 @@ function ReorderableList<T extends { id: string }>({
}
}}
longPressDelay={500}
onItemDragStart={() =>
onItemDragStart={async () => {
if (
customizableSidebarFeature &&
!customizableSidebarFeature?.isAllowed
) {
ToastManager.show({
message: customizableSidebarFeature?.error,
type: "info",
actionText: strings.upgrade(),
func: () => PaywallSheet.present(customizableSidebarFeature)
});
return;
}
useSideBarDraggingStore.setState({
dragging: true
})
}
disableVirtualization
});
}}
itemsDraggable={disableDefaultDrag ? dragging : true}
lockItemDragsToMainAxis
onItemReorder={({ fromIndex, fromItem, toIndex, toItem }) => {
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;
}
const newOrder = getOrderedItems().map((item) => item.id);
const element = newOrder.splice(fromIndex, 1)[0];
if (toIndex === 0) {

View File

@@ -0,0 +1,88 @@
/*
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>
);
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,96 +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 { 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

@@ -1,290 +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, { 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

@@ -1,218 +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, { 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

@@ -1,97 +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 { 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

@@ -1,318 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export const 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

@@ -1,93 +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 { 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

@@ -1,87 +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, { 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

@@ -1,144 +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, { 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

@@ -1,101 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { Platform, 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

@@ -1,735 +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 { 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

@@ -1,60 +0,0 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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 } from "../../services/event-manager";
import { eSendEvent, ToastManager } from "../../services/event-manager";
import Navigation from "../../services/navigation";
import { useMenuStore } from "../../stores/use-menu-store";
import { useRelationStore } from "../../stores/use-relation-store";
@@ -38,6 +38,8 @@ 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();
@@ -95,6 +97,7 @@ 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";
@@ -109,6 +112,24 @@ 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
@@ -133,10 +154,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
>
{!colorNotes || !colorNotes.length ? (
<Button
onPress={async () => {
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
onPress={onPress}
buttonType={{
text: colors.primary.accent
}}
@@ -172,10 +190,7 @@ export const ColorTags = ({ item }: { item: Note }) => {
marginRight: 5
}}
type="secondary"
onPress={() => {
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
onPress={onPress}
>
<Icon
testID="icon-plus"

View File

@@ -27,8 +27,9 @@ 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 } from "../../utils/size";
import { AppFontSize, defaultBorderRadius } 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";
@@ -145,7 +146,8 @@ export const Items = ({
key={item.id}
style={{
alignItems: "center",
width: columnItemWidth - 8
width: columnItemWidth - 8,
opacity: item.locked ? 0.5 : 1
}}
>
<Pressable
@@ -220,7 +222,8 @@ export const Items = ({
borderRadius: 0,
justifyContent: "flex-start",
alignSelf: "flex-start",
width: "100%"
width: "100%",
opacity: item.locked ? 0.5 : 1
}}
/>
),
@@ -244,7 +247,9 @@ export const Items = ({
key={item.id}
testID={"icon-" + item.id}
style={{
width: columnItemWidth - 8
width: columnItemWidth - 8,
alignSelf: "flex-start",
gap: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
>
<View
@@ -252,7 +257,13 @@ export const Items = ({
height: columnItemWidth / 2,
width: columnItemWidth - DefaultAppStyles.GAP_SMALL,
justifyContent: "center",
alignItems: "center"
alignItems: "center",
borderWidth: 1,
borderRadius: defaultBorderRadius,
borderColor: item.checked
? item.activeColor || colors.primary.accent
: colors.primary.border,
overflow: "hidden"
}}
>
<Icon
@@ -267,6 +278,28 @@ 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
@@ -324,16 +357,21 @@ export const Items = ({
paginationStyle={{
position: "relative",
marginHorizontal: 2,
marginBottom: 0,
marginTop: DefaultAppStyles.GAP
marginBottom: -10,
marginTop: 10
}}
contentContainerStyle={{
justifyContent: "flex-start",
alignItems: "flex-start",
alignSelf: "flex-start"
}}
centerContent={false}
renderItem={({ item, index }) => (
<View
style={{
flexDirection: "row",
paddingHorizontal: DefaultAppStyles.GAP,
gap: 5,
width: width
gap: 5
}}
>
{item.map(renderTopBarItem)}

View File

@@ -17,22 +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 { Button } from "../ui/button";
import { ColorTags } from "./color-tags";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import ManageTags from "../../screens/manage-tags";
import {
paddingTop,
paddingVertical
} from "deprecated-react-native-prop-types/DeprecatedLayoutPropTypes";
export const Tags = ({ item, close }) => {
const { colors } = useThemeColors();

View File

@@ -87,7 +87,7 @@ export const SelectionHeader = React.memo(
const restoreItem = async () => {
if (!selectedItemsList.length) return;
if ((await db.trash.restore(...selectedItemsList)) === false) return;
await db.trash.restore(...selectedItemsList);
Navigation.queueRoutesForUpdate();
clearSelection();

View File

@@ -224,7 +224,6 @@ const SheetProvider = ({ context = "global" }) => {
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
width="100%"
fontSize={AppFontSize.md}
/>
))}

View File

@@ -0,0 +1,495 @@
/*
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>
);
};

View File

@@ -48,7 +48,6 @@ import { DefaultAppStyles } from "../../../utils/styles";
import { sleep } from "../../../utils/time";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import { ProTag } from "../../premium/pro-tag";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
@@ -79,7 +78,6 @@ const ExportNotesSheet = ({
| undefined
>();
const [status, setStatus] = useState<string>();
const premium = useUserStore((state) => state.premium);
const exportNoteAs = async (
type: "pdf" | "txt" | "md" | "html" | "md-frontmatter"
@@ -123,8 +121,7 @@ const ExportNotesSheet = ({
await exportNoteAs("pdf");
},
icon: "file-pdf-box",
id: notesnook.ids.dialogs.export.pdf,
pro: premium
id: notesnook.ids.dialogs.export.pdf
},
{
title: "Markdown",
@@ -132,8 +129,7 @@ const ExportNotesSheet = ({
await exportNoteAs("md");
},
icon: "language-markdown",
id: notesnook.ids.dialogs.export.md,
pro: premium
id: notesnook.ids.dialogs.export.md
},
{
title: "Markdown + Frontmatter",
@@ -141,8 +137,7 @@ const ExportNotesSheet = ({
await exportNoteAs("md-frontmatter");
},
icon: "language-markdown",
id: notesnook.ids.dialogs.export.md,
pro: premium
id: notesnook.ids.dialogs.export.md
},
{
title: "Plain Text",
@@ -159,8 +154,7 @@ const ExportNotesSheet = ({
await exportNoteAs("html");
},
icon: "language-html5",
id: notesnook.ids.dialogs.export.html,
pro: premium
id: notesnook.ids.dialogs.export.html
}
];
@@ -216,9 +210,7 @@ const ExportNotesSheet = ({
>
<Icon
name={item.icon}
color={
item.pro ? colors.primary.accent : colors.primary.icon
}
color={colors.primary.icon}
size={AppFontSize.xxxl + 10}
/>
</View>
@@ -227,7 +219,6 @@ const ExportNotesSheet = ({
flexShrink: 1
}}
>
{!item.pro ? <ProTag size={12} /> : null}
<Heading style={{ marginLeft: 10 }} size={AppFontSize.md}>
{item.title}
</Heading>
@@ -298,7 +289,6 @@ const ExportNotesSheet = ({
}
type="accent"
width={250}
fontSize={AppFontSize.md}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
@@ -328,7 +318,6 @@ const ExportNotesSheet = ({
title={strings.share()}
type="secondaryAccented"
width={250}
fontSize={AppFontSize.md}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
@@ -360,7 +349,6 @@ const ExportNotesSheet = ({
title={strings.exportAgain()}
type="secondaryAccented"
width={250}
fontSize={AppFontSize.md}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}

View File

@@ -36,7 +36,6 @@ export const ShareComponent = ({ uri, name, padding }) => {
title={strings.open()}
type="accent"
width="100%"
fontSize={AppFontSize.md}
onPress={async () => {
FileViewer.open(uri, {
showOpenWithDialog: true,
@@ -54,7 +53,6 @@ export const ShareComponent = ({ uri, name, padding }) => {
title={strings.share()}
type="shade"
width="100%"
fontSize={AppFontSize.md}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}

View File

@@ -36,6 +36,7 @@ import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
import Config from "react-native-config";
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
const { colors } = useThemeColors();
@@ -67,7 +68,8 @@ App version: ${getVersion()}
Platform: ${Platform.OS}
Device: ${getBrand() || ""}-${getModel() || ""}-${getSystemVersion() || ""}
Pro: ${PremiumService.get()}
Logged in: ${user ? "yes" : "no"}`,
Logged in: ${user ? "yes" : "no"}
Github Release: ${Config.GITHUB_RELEASE === "true" ? "Yes" : "No"}`,
userId: user?.id
});
if (!issueUrl.current) {
@@ -159,7 +161,7 @@ Logged in: ${user ? "yes" : "no"}`,
borderColor: colors.primary.border,
borderRadius: defaultBorderRadius,
padding: DefaultAppStyles.GAP,
fontFamily: "OpenSans-Regular",
fontFamily: "Inter-Regular",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
fontSize: AppFontSize.md,
color: colors.primary.heading
@@ -192,7 +194,7 @@ Logged in: ${user ? "yes" : "no"}`,
borderColor: colors.primary.border,
borderRadius: defaultBorderRadius,
padding: DefaultAppStyles.GAP,
fontFamily: "OpenSans-Regular",
fontFamily: "Inter-Regular",
maxHeight: 200,
fontSize: AppFontSize.sm,
marginBottom: 2.5,

View File

@@ -16,6 +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 { useIsFeatureAvailable } from "@notesnook/common";
import {
ContentBlock,
Note,
@@ -33,12 +34,12 @@ import { db } from "../../../common/database";
import { useDBItem } from "../../../hooks/use-db-item";
import { editorController } from "../../../screens/editor/tiptap/utils";
import { presentSheet } from "../../../services/event-manager";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
const ListNoteItem = ({
id,
@@ -146,6 +147,7 @@ export default function LinkNote(props: {
onLinkCreated: () => void;
close?: (ctx?: string) => void;
}) {
const blockLinking = useIsFeatureAvailable("blockLinking");
const { colors } = useThemeColors();
const query = useRef<string>();
const [notes, setNotes] = useState<VirtualizedGrouping<Note>>();
@@ -327,7 +329,31 @@ export default function LinkNote(props: {
keyboardShouldPersistTaps="handled"
windowSize={3}
keyExtractor={(item) => item.id}
data={nodes}
ListEmptyComponent={
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL,
backgroundColor: colors.secondary.background,
padding: DefaultAppStyles.GAP,
borderRadius: defaultBorderRadius,
borderWidth: 0.5,
borderColor: colors.secondary.border,
alignItems: "center"
}}
>
<Paragraph color={colors.secondary.paragraph}>
{blockLinking?.error}
</Paragraph>
<Button
title={strings.upgradePlan()}
style={{
width: "100%"
}}
type="accent"
/>
</View>
}
data={blockLinking?.isAllowed ? nodes : []}
/>
) : (
<FlatList

View File

@@ -17,11 +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 { useAreFeaturesAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React from "react";
import { View } from "react-native";
import { eSendEvent, presentSheet } from "../../../services/event-manager";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { eCloseSheet } from "../../../utils/events";
import { SideMenuItem } from "../../../utils/menu-items";
@@ -31,9 +36,14 @@ import { useSideBarDraggingStore } from "../../side-menu/dragging-store";
import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import PaywallSheet from "../paywall";
export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
const { colors } = useThemeColors();
return (
const featuresAvailable = useAreFeaturesAvailable([
"customHomepage",
"customizableSidebar"
]);
return !featuresAvailable ? null : (
<View
style={{
width: "100%",
@@ -44,24 +54,52 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
{[
{
title: strings.setAsHomepage(),
onPress: () => {
onPress: async () => {
if (!featuresAvailable?.customHomepage.isAllowed) {
ToastManager.show({
message: featuresAvailable?.customHomepage.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(featuresAvailable?.customHomepage);
}
});
return;
}
SettingsService.setProperty("homepageV2", {
id: item.id,
type: "default"
});
eSendEvent(eCloseSheet);
},
icon: "home-outline"
icon: "home-outline",
type: "switch",
state: SettingsService.getProperty("homepageV2")?.id === item.id,
locked: !featuresAvailable?.customHomepage.isAllowed
},
{
title: strings.reorder(),
onPress: () => {
onPress: async () => {
if (!featuresAvailable?.customizableSidebar.isAllowed) {
ToastManager.show({
message: featuresAvailable?.customizableSidebar.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(featuresAvailable?.customizableSidebar);
}
});
return;
}
useSideBarDraggingStore.setState({
dragging: true
});
eSendEvent(eCloseSheet);
},
icon: "sort-ascending"
icon: "sort-ascending",
locked: !featuresAvailable?.customizableSidebar.isAllowed
}
].map((item) => (
<Pressable
@@ -73,18 +111,42 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
justifyContent: "flex-start",
gap: DefaultAppStyles.GAP_SMALL,
borderRadius: 0,
paddingHorizontal: DefaultAppStyles.GAP
paddingHorizontal: DefaultAppStyles.GAP,
opacity: item.locked ? 0.6 : 1
}}
onPress={() => {
item.onPress();
}}
>
<AppIcon
color={colors.secondary.icon}
name={item.icon}
size={AppFontSize.xl}
/>
<Paragraph>{item.title}</Paragraph>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: DefaultAppStyles.GAP_SMALL
}}
>
<AppIcon
color={colors.secondary.icon}
name={item.icon}
size={AppFontSize.xl}
/>
<Paragraph>{item.title}</Paragraph>
</View>
{item.locked ? (
<AppIcon
name="lock"
size={AppFontSize.lg}
color={colors.primary.icon}
style={{ marginLeft: "auto" }}
/>
) : item.type === "switch" && item.state ? (
<AppIcon
name="check"
size={AppFontSize.lg}
color={colors.primary.accent}
style={{ marginLeft: "auto" }}
/>
) : null}
</Pressable>
))}
</View>

View File

@@ -27,7 +27,8 @@ import NotebookScreen from "../../../screens/notebook";
import {
eSendEvent,
eSubscribeEvent,
presentSheet
presentSheet,
ToastManager
} from "../../../services/event-manager";
import {
createNotebookTreeStores,
@@ -44,6 +45,8 @@ import { NotebookItem } from "../../side-menu/notebook-item";
import { IconButton } from "../../ui/icon-button";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import { isFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../paywall";
const {
useNotebookExpandedStore,
@@ -167,7 +170,21 @@ export const Notebooks = (props: {
height: 30
}}
name="plus"
onPress={() => {
onPress={async () => {
const notebooksFeature = await isFeatureAvailable("notebooks");
if (!notebooksFeature.isAllowed) {
ToastManager.show({
message: notebooksFeature.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
ToastManager.hide();
PaywallSheet.present(notebooksFeature);
}
});
return;
}
AddNotebookSheet.present(undefined, props.rootNotebook, "local");
}}
/>

View File

@@ -0,0 +1,266 @@
import { FeatureId, FeatureResult } from "@notesnook/common";
import { SubscriptionPlan, SubscriptionProvider } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { useEffect } from "react";
import { Platform, View } from "react-native";
import Config from "react-native-config";
import usePricingPlans, {
PlanOverView
} from "../../../hooks/use-pricing-plans";
import {
eSendEvent,
presentSheet,
ToastManager
} from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
import { useUserStore } from "../../../stores/use-user-store";
import { eCloseSheet } from "../../../utils/events";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { AuthMode } from "../../auth/common";
import AppIcon from "../../ui/AppIcon";
import { Button } from "../../ui/button";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
const isGithubRelease = Config.GITHUB_RELEASE === "true";
const INDEX_TO_PLAN = {
1: "essential",
2: "pro",
3: "believer"
};
export default function PaywallSheet<Tid extends FeatureId>(props: {
feature: FeatureResult<Tid>;
}) {
const { colors } = useThemeColors();
const pricingPlans = usePricingPlans();
useEffect(() => {
ToastManager.hide();
if (!props.feature.availableOn) return;
const plan = pricingPlans.pricingPlans.find(
//@ts-ignore
(p) => p.id === INDEX_TO_PLAN[props.feature.availableOn]
);
if (!plan) return;
pricingPlans.selectPlan(plan?.id);
const product = isGithubRelease
? "yearly"
: plan?.subscriptionSkuList?.find((sku) => sku.includes("year"));
if (product) {
pricingPlans.selectProduct(product);
}
}, []);
const isSubscribedOnWeb =
PremiumService.get() &&
(pricingPlans.user?.subscription?.provider ===
SubscriptionProvider.PADDLE ||
pricingPlans.user?.subscription?.provider ===
SubscriptionProvider.STREETWRITERS);
const isCurrentPlatform =
(pricingPlans.user?.subscription?.provider === SubscriptionProvider.APPLE &&
Platform.OS === "ios") ||
(pricingPlans.user?.subscription?.provider ===
SubscriptionProvider.GOOGLE &&
Platform.OS === "android");
return !pricingPlans.currentPlan ? null : (
<View
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<>
<View
style={{
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph>
<AppIcon
name="crown"
size={AppFontSize.md}
color={colors.static.orange}
/>
{strings.upgradePlanTo(pricingPlans.currentPlan?.name)}
</Paragraph>
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL_SMALL,
width: "100%"
}}
>
<Heading>{strings.tryItForFree()}</Heading>
<Heading size={AppFontSize.sm}>
{strings.getThisAndSoMuchMore()}
</Heading>
<View
style={{
gap: DefaultAppStyles.GAP_SMALL,
flexDirection: "row"
}}
>
<AppIcon name="cloud" size={AppFontSize.xxl} />
<Paragraph
style={{
flexShrink: 1
}}
>
<Heading size={AppFontSize.sm}>
{
PlanOverView[
pricingPlans.currentPlan.id as keyof typeof PlanOverView
].storage
}
</Heading>{" "}
{strings.cloudSpace()}
</Paragraph>
</View>
{pricingPlans.currentPlan.id !== "essential" ? (
<View
style={{
gap: DefaultAppStyles.GAP_SMALL,
flexDirection: "row"
}}
>
<AppIcon name="lock" size={AppFontSize.xxl} />
<Paragraph
style={{
flexShrink: 1
}}
>
<Heading size={AppFontSize.sm}>{strings.appLock()}</Heading>{" "}
{strings.appLockFeatureBenefit()}
</Paragraph>
</View>
) : null}
<View
style={{
gap: DefaultAppStyles.GAP_SMALL,
flexDirection: "row"
}}
>
<AppIcon name="vector-link" size={AppFontSize.xxl} />
<Paragraph
style={{
flexShrink: 1
}}
>
{strings.advancedNoteTaking[0]()}{" "}
<Heading size={AppFontSize.sm}>
{strings.advancedNoteTaking[1]()}
</Heading>{" "}
{strings.advancedNoteTaking[2]()}
</Paragraph>
</View>
</View>
</View>
<View
style={{
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<Paragraph
style={{
marginVertical: 10
}}
size={AppFontSize.xs}
>
<Heading size={AppFontSize.xs}>{strings.cancelAnytime()}</Heading>{" "}
{strings.googleReminderTrial()}
</Paragraph>
<Button
type="accent"
title={strings.upgrade()}
style={{
marginVertical: DefaultAppStyles.GAP_VERTICAL,
width: "100%"
}}
onPress={() => {
if (PremiumService.get()) {
if (
pricingPlans.user?.subscription.plan ===
SubscriptionPlan.LEGACY_PRO ||
!isCurrentPlatform
) {
ToastManager.show({
message: strings.cannotChangePlan(),
context: "local"
});
return;
}
if (isSubscribedOnWeb) {
ToastManager.show({
message: strings.changePlanOnWeb(),
context: "local"
});
return;
}
}
eSendEvent(eCloseSheet);
if (!useUserStore.getState().user) {
Navigation.navigate("Auth", {
mode: AuthMode.login
});
return;
}
Navigation.navigate("PayWall", {
context: "logged-in",
state: {
planId: pricingPlans.currentPlan?.id,
productId: isGithubRelease
? "yearly"
: (pricingPlans.selectProduct as any).productId,
billingType: "annual"
}
});
}}
/>
</View>
{isSubscribedOnWeb ? null : (
<Button
type="plain"
title={strings.exploreAllPlans()}
icon="arrow-right"
iconPosition="right"
onPress={() => {
eSendEvent(eCloseSheet);
Navigation.navigate("PayWall", {
context: useUserStore.getState().user
? "logged-in"
: "logged-out"
});
}}
/>
)}
</>
</View>
);
}
PaywallSheet.present = <Tid extends FeatureId>(feature: FeatureResult<Tid>) => {
if (SettingsService.getProperty("serverUrls")) return;
presentSheet({
component: <PaywallSheet feature={feature} />
});
};

View File

@@ -0,0 +1,132 @@
import {
FeatureUsage,
formatBytes,
getFeature,
getFeaturesUsage
} from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { useEffect, useState } from "react";
import { Platform, View } from "react-native";
import { ScrollView } from "react-native-actions-sheet";
import { eSendEvent, ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { eCloseSheet } from "../../../utils/events";
import { AppFontSize } 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";
import { SubscriptionPlan, SubscriptionProvider } from "@notesnook/core";
import { useUserStore } from "../../../stores/use-user-store";
import PremiumService from "../../../services/premium";
import SettingsService from "../../../services/settings";
export function PlanLimits() {
const { colors } = useThemeColors();
const [featureUsage, setFeatureUsage] = useState<FeatureUsage[]>();
const user = useUserStore((state) => state.user);
useEffect(() => {
getFeaturesUsage()
.then((result) => {
setFeatureUsage(result);
})
.catch((e) => console.log(e));
}, []);
const isCurrentPlatform =
(user?.subscription?.provider === SubscriptionProvider.APPLE &&
Platform.OS === "ios") ||
(user?.subscription?.provider === SubscriptionProvider.GOOGLE &&
Platform.OS === "android");
return (
<ScrollView
style={{
paddingHorizontal: DefaultAppStyles.GAP,
width: "100%",
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
contentContainerStyle={{
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<Heading>{strings.planLimits()}</Heading>
{featureUsage?.map((item) => (
<View
key={item.id}
style={{
gap: DefaultAppStyles.GAP_VERTICAL_SMALL,
width: "100%"
}}
>
<View
style={{
flexDirection: "row",
width: "100%",
justifyContent: "space-between"
}}
>
<Paragraph size={AppFontSize.sm}>
{getFeature(item.id).title}
</Paragraph>
<Paragraph size={AppFontSize.sm}>
{item.total === Infinity
? strings.unlimited()
: item.id === "storage"
? `${formatBytes(item.used)}/${formatBytes(
item.total
)} ${strings.used()}`
: `${item.used}/${item.total} ${strings.used()}`}
</Paragraph>
</View>
</View>
))}
{((user?.subscription?.provider === SubscriptionProvider.PADDLE ||
user?.subscription?.provider === SubscriptionProvider.STREETWRITERS ||
!isCurrentPlatform) &&
PremiumService.get()) ||
SettingsService.getProperty("serverUrls") ? null : (
<Button
title={strings.changePlan()}
onPress={() => {
if (user?.subscription?.plan === SubscriptionPlan.LEGACY_PRO) {
ToastManager.show({
message: strings.cannotChangePlan(),
context: "local"
});
return;
}
if (
user?.subscription.plan !== SubscriptionPlan.FREE &&
user?.subscription.productId.includes("5year")
) {
ToastManager.show({
message:
"You have made a one time purchase. To change your plan please contact support.",
type: "info",
context: "local"
});
return;
}
Navigation.navigate("PayWall", {
context: "logged-in",
canGoBack: true
});
eSendEvent(eCloseSheet);
}}
type="accent"
fontSize={AppFontSize.xs}
style={{
width: "100%",
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
/>
)}
</ScrollView>
);
}

View File

@@ -17,49 +17,70 @@ 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 { hosts, Monograph, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import Clipboard from "@react-native-clipboard/clipboard";
import React, { useRef, useState } from "react";
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import React, { useEffect, useRef, useState } from "react";
import {
ActivityIndicator,
TextInput,
TouchableOpacity,
View
} from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import ToggleSwitch from "toggle-switch-react-native";
import { db } from "../../../common/database";
import { requestInAppReview } from "../../../services/app-review";
import { presentSheet, ToastManager } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import { useAttachmentStore } from "../../../stores/use-attachment-store";
import { useThemeColors } from "@notesnook/theme";
import { openLinkInBrowser } from "../../../utils/functions";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import DialogHeader from "../../dialog/dialog-header";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Seperator from "../../ui/seperator";
import Heading from "../../ui/typography/heading";
import Paragraph from "../../ui/typography/paragraph";
import { requestInAppReview } from "../../../services/app-review";
import { hosts, Note } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
const PublishNoteSheet = ({
note: item
note
}: {
note: Note;
close?: (ctx?: string) => void;
}) => {
const { colors } = useThemeColors();
const attachmentDownloads = useAttachmentStore((state) => state.downloading);
const downloading = attachmentDownloads?.[`monograph-${item.id}`];
const downloading = attachmentDownloads?.[`monograph-${note.id}`];
const [selfDestruct, setSelfDestruct] = useState(false);
const [isLocked, setIsLocked] = useState(false);
const [note, setNote] = useState<Note | undefined>(item);
const [monograph, setMonograph] = useState<Monograph>();
const [publishing, setPublishing] = useState(false);
const publishUrl =
note && `${hosts.MONOGRAPH_HOST}/${db.monographs.monograph(note?.id)}`;
const isPublished = note && db.monographs.isPublished(note?.id);
const pwdInput = useRef(null);
const publishUrl = monograph && `${hosts.MONOGRAPH_HOST}/${monograph?.id}`;
const isPublished = !!monograph;
const pwdInput = useRef<TextInput>(null);
const passwordValue = useRef<string>();
useEffect(() => {
(async () => {
const monograph = await db.monographs.get(
db.monographs.monograph(note.id)
);
setMonograph(monograph);
if (monograph) {
setSelfDestruct(!!monograph?.selfDestruct);
if (monograph.password) {
passwordValue.current = await db.monographs.decryptPassword(
monograph?.password
);
setIsLocked(!!monograph?.password);
}
}
})();
}, []);
const publishNote = async () => {
if (publishing) return;
setPublishLoading(true);
@@ -71,7 +92,8 @@ const PublishNoteSheet = ({
selfDestruct: selfDestruct,
password: isLocked ? passwordValue.current : undefined
});
setNote(await db.notes.note(note.id));
setMonograph(await db.monographs.get(db.monographs.monograph(note.id)));
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
@@ -98,7 +120,7 @@ const PublishNoteSheet = ({
try {
if (note?.id) {
await db.monographs.unpublish(note.id);
setNote(await db.notes.note(note.id));
setMonograph(undefined);
Navigation.queueRoutesForUpdate();
setPublishLoading(false);
}
@@ -118,7 +140,8 @@ const PublishNoteSheet = ({
style={{
width: "100%",
alignSelf: "center",
paddingHorizontal: DefaultAppStyles.GAP
paddingHorizontal: DefaultAppStyles.GAP,
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<DialogHeader
@@ -218,37 +241,54 @@ const PublishNoteSheet = ({
style={{
flexDirection: "row",
alignItems: "center",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
backgroundColor: colors.secondary.background,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
borderRadius: defaultBorderRadius,
marginTop: DefaultAppStyles.GAP_VERTICAL
paddingHorizontal: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
<IconButton
onPress={() => {
if (publishing) return;
setIsLocked(!isLocked);
}}
color={isLocked ? colors.selected.icon : colors.primary.icon}
size={AppFontSize.xl}
name={
isLocked
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<View
style={{
width: "100%",
flexShrink: 1
}}
>
<Heading size={AppFontSize.md}>
{strings.monographPassHeading()}
</Heading>
<View
style={{
flexDirection: "row",
justifyContent: "space-between"
}}
>
<Heading size={AppFontSize.md}>
{strings.monographPassHeading()}
</Heading>
<ToggleSwitch
isOn={isLocked}
onColor={colors.primary.accent}
offColor={colors.primary.icon}
size="small"
animationSpeed={150}
onToggle={() => setIsLocked(!isLocked)}
/>
</View>
<Paragraph>{strings.monographPassDesc()}</Paragraph>
{isLocked ? (
<>
<Input
fwdRef={pwdInput}
onChangeText={(value) => (passwordValue.current = value)}
blurOnSubmit
secureTextEntry
defaultValue={passwordValue.current}
placeholder={strings.enterPassword()}
containerStyle={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
/>
</>
) : null}
</View>
</TouchableOpacity>
@@ -262,31 +302,35 @@ const PublishNoteSheet = ({
alignItems: "center",
backgroundColor: colors.secondary.background,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
borderRadius: defaultBorderRadius
borderRadius: defaultBorderRadius,
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<IconButton
onPress={() => {
setSelfDestruct(!selfDestruct);
}}
color={selfDestruct ? colors.selected.icon : colors.primary.icon}
size={AppFontSize.xl}
name={
selfDestruct
? "check-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<View
style={{
width: "100%",
flexShrink: 1
}}
>
<Heading size={AppFontSize.md}>
{strings.monographSelfDestructHeading()}
</Heading>
<View
style={{
flexDirection: "row",
justifyContent: "space-between"
}}
>
<Heading size={AppFontSize.md}>
{strings.monographSelfDestructHeading()}
</Heading>
<ToggleSwitch
isOn={selfDestruct}
onColor={colors.primary.accent}
offColor={colors.primary.icon}
size="small"
animationSpeed={150}
onToggle={() => setSelfDestruct(!selfDestruct)}
/>
</View>
<Paragraph>{strings.monographSelfDestructDesc()}</Paragraph>
</View>
</TouchableOpacity>
@@ -294,56 +338,33 @@ const PublishNoteSheet = ({
<View
style={{
width: "100%",
alignSelf: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL
justifyContent: "center",
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
{isLocked ? (
<>
<Input
fwdRef={pwdInput}
onChangeText={(value) => (passwordValue.current = value)}
blurOnSubmit
secureTextEntry
defaultValue={passwordValue.current}
placeholder={strings.enterPassword()}
/>
<Seperator half />
</>
) : null}
<View
<Button
onPress={publishNote}
style={{
flexDirection: "row",
width: "100%",
justifyContent: "center"
borderRadius: defaultBorderRadius
}}
>
{isPublished && (
<>
<Button
onPress={deletePublishedNote}
fontSize={AppFontSize.md}
type="error"
title={strings.unpublish()}
style={{
width: "49%"
}}
/>
</>
)}
<Seperator half />
<Button
onPress={publishNote}
fontSize={AppFontSize.md}
style={{
width: isPublished ? "49%" : 250,
borderRadius: isPublished ? 5 : 100
}}
type="accent"
title={isPublished ? strings.update() : strings.publish()}
/>
</View>
type="accent"
title={isPublished ? strings.update() : strings.publish()}
/>
{isPublished && (
<>
<Button
onPress={deletePublishedNote}
type="error"
title={strings.unpublish()}
style={{
width: "100%",
borderRadius: defaultBorderRadius
}}
/>
</>
)}
</View>
</>
)}

View File

@@ -93,7 +93,6 @@ const RateAppSheet = () => {
<Seperator half />
<Button
onPress={rateApp}
fontSize={AppFontSize.md}
width="100%"
type="accent"
title={strings.rateApp()}
@@ -116,14 +115,12 @@ const RateAppSheet = () => {
setVisible(false);
clearMessage();
}}
fontSize={AppFontSize.md}
type="error"
width="48%"
title={strings.never()}
/>
<Button
onPress={onClose}
fontSize={AppFontSize.md}
width="48%"
type="secondary"
title={strings.later()}

View File

@@ -295,7 +295,6 @@ class RecoveryKeySheet extends React.Component {
title={strings.copyToClipboard()}
width="100%"
type="secondaryAccented"
fontSize={AppFontSize.md}
/>
<Seperator />
<Button
@@ -303,7 +302,6 @@ class RecoveryKeySheet extends React.Component {
onPress={this.saveQRCODE}
width="100%"
type="secondaryAccented"
fontSize={AppFontSize.md}
icon="qrcode"
/>
<Seperator />
@@ -313,7 +311,6 @@ class RecoveryKeySheet extends React.Component {
width="100%"
type="secondaryAccented"
icon="text"
fontSize={AppFontSize.md}
/>
<Seperator />
@@ -323,7 +320,6 @@ class RecoveryKeySheet extends React.Component {
width="100%"
type="secondaryAccented"
icon="cloud"
fontSize={AppFontSize.md}
/>
<Seperator />
@@ -344,7 +340,6 @@ class RecoveryKeySheet extends React.Component {
title={strings.done()}
width="100%"
type="error"
fontSize={AppFontSize.md}
onPress={this.close}
/>
</View>

View File

@@ -56,6 +56,8 @@ import {
import { useSideBarDraggingStore } from "./dragging-store";
import { Button } from "../ui/button";
import SettingsService from "../../services/settings";
import { isFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../sheets/paywall";
const renderScene = SceneMap({
home: SideMenuHome,
notebooks: SideMenuNotebooks,
@@ -347,10 +349,23 @@ const TabBar = (
testID="sidebar-add-button"
size={AppFontSize.lg - 2}
color={colors.primary.icon}
onPress={() => {
onPress={async () => {
if (props.navigationState.index === 1) {
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
if (!notebooksFeature.isAllowed) {
PaywallSheet.present(notebooksFeature);
return;
}
AddNotebookSheet.present();
} else {
const tagsFeature = await isFeatureAvailable("tags");
if (!tagsFeature.isAllowed) {
PaywallSheet.present(tagsFeature);
return;
}
presentDialog({
title: strings.addTag(),
paragraph: strings.addTagDesc(),

View File

@@ -16,34 +16,36 @@ 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 { SubscriptionPlan } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { Fragment } from "react";
import React from "react";
import { FlatList, View } from "react-native";
import { DraxProvider, DraxScrollView } from "react-native-drax";
import { db } from "../../common/database";
import Navigation from "../../services/navigation";
import SettingsService from "../../services/settings";
import { useMenuStore } from "../../stores/use-menu-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { useUserStore } from "../../stores/use-user-store";
import { MenuItemsList } from "../../utils/menu-items";
import { DefaultAppStyles } from "../../utils/styles";
import ReorderableList from "../list/reorderable-list";
import { MenuItemProperties } from "../sheets/menu-item-properties";
import { Button } from "../ui/button";
import { ColorSection } from "./color-section";
import { MenuItem } from "./menu-item";
import { PinnedSection } from "./pinned-section";
import { SideMenuHeader } from "./side-menu-header";
import { SUBSCRIPTION_STATUS } from "../../utils/constants";
import { eSendEvent } from "../../services/event-manager";
import { eOpenPremiumDialog } from "../../utils/events";
import { useUserStore } from "../../stores/use-user-store";
import { Button } from "../ui/button";
import { MenuItemProperties } from "../sheets/menu-item-properties";
const pro = {
title: strings.getNotesnookPro(),
title: strings.upgradePlan(),
icon: "crown",
id: "pro",
onPress: () => {
eSendEvent(eOpenPremiumDialog);
Navigation.navigate("PayWall", {
context: "logged-in"
});
}
};
@@ -58,8 +60,9 @@ export function SideMenuHome() {
state.hiddenItems["routes"]
]);
const subscriptionType = useUserStore(
(state) => state.user?.subscription?.type
(state) => state.user?.subscription?.plan
);
const user = useUserStore.getState().user;
return (
<View
@@ -137,19 +140,16 @@ export function SideMenuHome() {
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
{subscriptionType === SUBSCRIPTION_STATUS.TRIAL ||
subscriptionType === SUBSCRIPTION_STATUS.BASIC ? (
{(subscriptionType === SubscriptionPlan.FREE ||
!subscriptionType ||
!user) &&
!SettingsService.getProperty("serverUrls") ? (
<Button
title={pro.title}
iconColor={colors.static.yellow}
textStyle={{
color: colors.static.white
}}
icon={pro.icon}
style={{
backgroundColor: colors.static.black,
width: "100%"
}}
type="accent"
onPress={pro.onPress}
/>
) : null}

View File

@@ -17,26 +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 { useThemeColors } from "@notesnook/theme";
import React from "react";
import {
ActivityIndicator,
ColorValue,
DimensionValue,
TextStyle,
View,
ViewStyle,
useWindowDimensions
} from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useUserStore } from "../../../stores/use-user-store";
import { defaultBorderRadius, AppFontSize } from "../../../utils/size";
import { AppFontSize, defaultBorderRadius } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import NativeTooltip from "../../../utils/tooltip";
import { ProTag } from "../../premium/pro-tag";
import { Pressable, PressableProps, useButton } from "../pressable";
import Heading from "../typography/heading";
import Paragraph from "../typography/paragraph";
import { DefaultAppStyles } from "../../../utils/styles";
export interface ButtonProps extends PressableProps {
height?: number;
icon?: string;
@@ -85,8 +81,6 @@ export const Button = ({
iconStyle,
...restProps
}: ButtonProps) => {
const { colors } = useThemeColors();
const premium = useUserStore((state) => state.premium);
const { text } = useButton({
type,
accent: accentColor,
@@ -167,15 +161,6 @@ export const Button = ({
{title}
</Component>
)}
{proTag && !premium ? (
<View
style={{
marginLeft: 10
}}
>
<ProTag size={10} width={40} background={colors.primary.shade} />
</View>
) : null}
{icon && !loading && iconPosition === "right" ? (
<Icon

View File

@@ -237,9 +237,9 @@ const Input = ({
onPress && loading ? colors.primary.accent : colors.primary.paragraph,
flexGrow: 1,
flexShrink: 1,
paddingBottom: DefaultAppStyles.GAP_VERTICAL - 2,
paddingTop: DefaultAppStyles.GAP_VERTICAL - 2,
fontFamily: "OpenSans-Regular",
paddingBottom: DefaultAppStyles.GAP_VERTICAL - 4,
paddingTop: DefaultAppStyles.GAP_VERTICAL - 4,
fontFamily: "Inter-Regular",
...(inputStyle as ViewStyle)
};

View File

@@ -25,9 +25,9 @@ import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets"
import { useSettingStore } from "../../../stores/use-setting-store";
import { useUserStore } from "../../../stores/use-user-store";
import { getContainerBorder } from "../../../utils/colors";
import { PremiumToast } from "../../premium/premium-toast";
import { Toast } from "../../toast";
import { NotesnookModule } from "../../../utils/notesnook-module";
import { Toast } from "../../toast";
/**
*
* @param {any} param0
@@ -63,14 +63,12 @@ const SheetWrapper = ({
let width = dimensions.width > 600 ? 600 : 500;
const isGestureNavigationEnabled =
NotesnookModule.isGestureNavigationEnabled();
const style = React.useMemo(() => {
return {
width: largeTablet || smallTablet ? width : "100%",
backgroundColor: colors.primary.background,
zIndex: 10,
paddingTop: 5,
paddingBottom: 0,
borderTopRightRadius: 15,
borderTopLeftRadius: 15,
alignSelf: "center",
@@ -78,9 +76,12 @@ const SheetWrapper = ({
borderBottomLeftRadius: 0,
...getContainerBorder(colors.primary.border, 0.5),
borderBottomWidth: 0,
paddingBottom: isGestureNavigationEnabled
? insets.bottom
: insets.bottom || 48
paddingBottom:
Platform.OS === "android" && !insets.bottom
? isGestureNavigationEnabled
? 0
: 30
: 0
};
}, [
colors.primary.background,
@@ -131,6 +132,10 @@ const SheetWrapper = ({
width: 100,
backgroundColor: colors.secondary.background
}}
safeAreaInsets={{
...insets,
bottom: Platform.OS === "android" ? 0 : insets.bottom
}}
statusBarTranslucent
drawUnderStatusBar={true}
containerStyle={style}
@@ -155,11 +160,6 @@ const SheetWrapper = ({
ExtraOverlayComponent={
<>
{overlay}
<PremiumToast
context="sheet"
close={() => fwdRef?.current?.hide()}
offset={50}
/>
<Toast context="local" />
</>
}
@@ -169,10 +169,7 @@ const SheetWrapper = ({
{bottomPadding ? (
<View
style={{
height:
Platform.OS === "ios" && insets.bottom !== 0
? insets.bottom + 5
: 20
height: insets.bottom !== 0 ? insets.bottom : 20
}}
/>
) : null}

View File

@@ -17,20 +17,20 @@ 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 { LayoutAnimation, View } from "react-native";
import { MMKV } from "../../common/database/mmkv";
import { eSendEvent, presentSheet } from "../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { eCloseSheet } from "../../utils/events";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { Button } from "../ui/button";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import walkthroughs, { TStep } from "./walkthroughs";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
export const Walkthrough = ({
steps,
canSkip = true
@@ -53,12 +53,20 @@ export const Walkthrough = ({
justifyContent: "center",
alignItems: "center",
padding: DefaultAppStyles.GAP,
paddingBottom: 0
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
{step.walkthroughItem(colors)}
{step.title ? <Heading>{step.title}</Heading> : null}
{step.title ? (
<Heading
style={{
textAlign: "center"
}}
>
{step.title}
</Heading>
) : null}
{step.text ? (
<Paragraph
style={{
@@ -74,8 +82,7 @@ export const Walkthrough = ({
{step.actionButton && (
<Button
style={{
height: 30,
marginTop: DefaultAppStyles.GAP_VERTICAL
height: 30
}}
textStyle={{
textDecorationLine: "underline"
@@ -89,11 +96,6 @@ export const Walkthrough = ({
)}
<Button
style={{
borderRadius: 100,
height: 40,
marginTop: DefaultAppStyles.GAP
}}
onPress={async () => {
switch (step.button?.type) {
case "next":
@@ -114,7 +116,6 @@ export const Walkthrough = ({
{canSkip ? (
<Button
style={{
marginTop: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
textStyle={{
@@ -148,18 +149,18 @@ Walkthrough.init = async () => {
};
Walkthrough.present = async (
id: "notebooks" | "trialstarted" | "emailconfirmed" | "prouser",
id: "emailconfirmed" | "prouser",
canSkip = true,
nopersist?: boolean
) => {
if (!nopersist) {
if (!walkthroughState || Object.keys(walkthroughState).length === 0) {
await Walkthrough.init();
}
if (walkthroughState[id]) return;
Walkthrough.update(id);
}
const walkthrough = walkthroughs[id];
// if (!nopersist) {
// if (!walkthroughState || Object.keys(walkthroughState).length === 0) {
// await Walkthrough.init();
// }
// if (walkthroughState[id]) return;
// Walkthrough.update(id);
// }
const walkthrough = walkthroughs[id]();
if (!walkthrough) return;
presentSheet({
component: <Walkthrough canSkip={canSkip} steps={walkthrough.steps} />,

View File

@@ -40,6 +40,10 @@ import { SvgView } from "../ui/svg";
import Heading from "../ui/typography/heading";
import Paragraph from "../ui/typography/paragraph";
import { DefaultAppStyles } from "../../utils/styles";
import { useUserStore } from "../../stores/use-user-store";
import { planToId, SubscriptionPlan } from "@notesnook/core";
import { planToDisplayName } from "../../utils/constants";
import AppIcon from "../ui/AppIcon";
export type TStep = {
text?: string;
@@ -56,282 +60,7 @@ export type TStep = {
};
};
const NotebookWelcome = () => {
const { colors } = useThemeColors();
const data = useRotator([
{
title: strings.workAndOffice(),
description: strings.workAndOfficeDesc(),
count: 2
},
{
title: strings.schoolWork(),
description: strings.schoolWorkDesc(),
count: 5
},
{
title: strings.recipes(),
description: strings.recipesDesc(),
count: 10
}
]);
return (
<View
style={{
width: "100%",
padding: DefaultAppStyles.GAP,
backgroundColor: colors.secondary.background,
borderRadius: 10,
...getContainerBorder(colors.secondary.background)
}}
>
<View
style={{
padding: DefaultAppStyles.GAP,
width: "100%",
backgroundColor: colors.primary.background,
...getElevationStyle(3),
borderRadius: 10,
marginVertical: DefaultAppStyles.GAP
}}
>
<Heading size={AppFontSize.md} color={colors.primary.heading}>
{data?.title}
</Heading>
<Paragraph>{data?.description}</Paragraph>
<Paragraph
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
{strings.dataTypesCamelCase.notebook()} - {data?.count}{" "}
{strings.dataTypesPlural.note()}
</Paragraph>
</View>
</View>
);
};
const notebooks: { id: string; steps: TStep[] } = {
id: "notebooks",
steps: [
{
title: strings.notebooks(),
text: strings.boostProductivityNotebook(),
walkthroughItem: () => <NotebookWelcome />,
button: {
type: "next",
title: strings.next()
}
},
{
title: strings.notebookNotes(),
text: strings.notebookNotesDesc(),
walkthroughItem: (colors: any) => (
<View
style={{
width: "100%",
padding: DefaultAppStyles.GAP,
backgroundColor: colors.secondary.background,
borderRadius: 10,
...getContainerBorder(colors.secondary.background)
}}
>
<View
style={{
padding: DefaultAppStyles.GAP,
width: "100%",
backgroundColor: colors.primary.background,
...getElevationStyle(3),
borderRadius: 10,
marginVertical: 12
}}
>
<Heading size={AppFontSize.md} color={colors.primary.heading}>
{strings.workAndOffice()}
</Heading>
<Paragraph>{strings.workAndOfficeDesc()}</Paragraph>
<Paragraph
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL_SMALL
}}
size={AppFontSize.xs}
color={colors.secondary.paragraph}
>
{strings.notes(2)}
</Paragraph>
</View>
<View
style={{
padding: DefaultAppStyles.GAP,
width: "90%",
backgroundColor: colors.primary.background,
borderRadius: 10,
alignSelf: "flex-end",
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph color={colors.primary.accent}>
<Icon
color={colors.primary.accent}
size={AppFontSize.sm}
name="bookmark"
/>{" "}
{strings.tasks()}
</Paragraph>
</View>
<View
style={{
padding: DefaultAppStyles.GAP,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
width: "80%",
backgroundColor: colors.primary.background,
borderRadius: defaultBorderRadius,
alignSelf: "flex-end",
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph size={AppFontSize.xs}>
<Icon
color={colors.primary.icon}
size={AppFontSize.sm}
name="note"
/>{" "}
{strings.taskAValue()}
</Paragraph>
</View>
<View
style={{
padding: DefaultAppStyles.GAP,
width: "80%",
backgroundColor: colors.primary.background,
borderRadius: defaultBorderRadius,
paddingVertical: DefaultAppStyles.GAP_VERTICAL,
alignSelf: "flex-end",
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph size={AppFontSize.xs}>
<Icon
color={colors.primary.icon}
size={AppFontSize.sm}
name="note"
/>{" "}
{strings.taskBValue()}
</Paragraph>
</View>
<View
style={{
padding: DefaultAppStyles.GAP,
width: "90%",
backgroundColor: colors.primary.background,
borderRadius: 10,
alignSelf: "flex-end",
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
>
<Paragraph color={colors.primary.accent}>
<Icon
color={colors.primary.accent}
size={AppFontSize.sm}
name="bookmark"
/>{" "}
{strings.meetings()}
</Paragraph>
</View>
</View>
),
button: {
type: "next",
title: strings.next()
}
},
{
title: strings.easyAccess(),
text: strings.easyAccessDesc(),
walkthroughItem: () => (
<View
style={{
paddingHorizontal: 20,
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
{/* <PinItem
isPlaceholder={true}
item={
{
title: strings.tasks(),
type: "notebook"
} as Notebook
}
onPress={() => {}}
/>
<PinItem
isPlaceholder={true}
item={
{
title: strings.workAndOffice(),
type: "notebook"
} as Notebook
}
onPress={() => {}}
/> */}
</View>
),
button: {
type: "done",
title: strings.addFirstNotebook(),
action: () => {
eSendEvent(eOpenAddNotebookDialog);
}
}
}
]
};
const trialstarted: { id: string; steps: TStep[] } = {
id: "trialstarted",
steps: [
{
title: strings.trialStarted(),
text: strings.trialStartedDesc(),
walkthroughItem: (colors) => (
<SvgView src={LAUNCH_ROCKET(colors.primary.paragraph)} />
),
button: {
type: "next",
title: strings.next()
}
},
{
title: strings.joinTheCause(),
text: strings.meetPrivacyMinded(),
walkthroughItem: (colors) => (
<SvgView src={COMMUNITY_SVG(colors.primary.paragraph)} />
),
button: {
type: "done",
title: strings.continue()
},
actionButton: {
text: strings.joinDiscord(),
action: () => {
Linking.openURL("https://discord.gg/zQBK97EE22").catch(() => {
/* empty */
});
}
}
}
]
};
const emailconfirmed: { id: string; steps: TStep[] } = {
const emailconfirmed: () => { id: string; steps: TStep[] } = () => ({
id: "emailconfirmed",
steps: [
{
@@ -346,105 +75,29 @@ const emailconfirmed: { id: string; steps: TStep[] } = {
}
}
]
};
});
const Support = () => {
return (
<View
style={{
width: "100%",
alignItems: "center"
}}
>
<SvgView src={SUPPORT_SVG()} />
<Heading>{strings.prioritySupport()}</Heading>
<Paragraph
style={{
textAlign: "center"
}}
size={AppFontSize.md}
>
{strings.weAreAlwaysListening()}
</Paragraph>
<Seperator />
<Button
style={{
justifyContent: "flex-start",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
width: "90%"
}}
onPress={() => {
Linking.openURL("https://discord.gg/zQBK97EE22").catch(() => {
/* empty */
});
}}
icon="discord"
type="secondary"
title={strings.joinDiscord()}
/>
<Button
style={{
justifyContent: "flex-start",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
width: "90%"
}}
onPress={() => {
Linking.openURL("https://t.me/notesnook").catch(() => {
/* empty */
});
}}
icon="telegram"
type="secondary"
title={strings.joinTelegram()}
/>
<Button
style={{
justifyContent: "flex-start",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
width: "90%"
}}
icon="bug"
type="secondary"
title={strings.reportAnIssue()}
/>
<Button
style={{
justifyContent: "flex-start",
marginBottom: DefaultAppStyles.GAP_VERTICAL,
width: "90%"
}}
icon="mail"
type="secondary"
title={strings.emailSupport()}
/>
</View>
);
};
const prouser: { id: string; steps: TStep[] } = {
id: "prouser",
steps: [
{
title: strings.welcomeToNotesnookPro(),
text: strings.thankYouPrivacy(),
walkthroughItem: (colors) => (
<SvgView src={LAUNCH_ROCKET(colors.primary.paragraph)} />
),
button: {
type: "next",
title: strings.next()
const prouser: () => { id: string; steps: TStep[] } = () => {
return {
id: "prouser",
steps: [
{
title: strings.welcomeToPlan(
planToDisplayName(
useUserStore.getState().user?.subscription?.plan as SubscriptionPlan
)
),
text: strings.thankYouPrivacy(),
walkthroughItem: (colors) => (
<AppIcon name="check" color={colors.primary.accent} size={50} />
),
button: {
type: "done",
title: strings.continue()
}
}
},
{
walkthroughItem: () => <Support />,
button: {
type: "done",
title: strings.continue()
}
}
]
]
};
};
export default { notebooks, trialstarted, emailconfirmed, prouser };
export default { emailconfirmed, prouser };

View File

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/* eslint-disable no-inner-declarations */
import { useAreFeaturesAvailable } from "@notesnook/common";
import {
createInternalLink,
Item,
@@ -38,11 +39,13 @@ import { presentDialog } from "../components/dialog/functions";
import NoteHistory from "../components/note-history";
import { AddNotebookSheet } from "../components/sheets/add-notebook";
import ExportNotesSheet from "../components/sheets/export-notes";
import PaywallSheet from "../components/sheets/paywall";
import PublishNoteSheet from "../components/sheets/publish-note";
import { ReferencesList } from "../components/sheets/references";
import { RelationsList } from "../components/sheets/relations-list/index";
import { useSideBarDraggingStore } from "../components/side-menu/dragging-store";
import { ButtonProps } from "../components/ui/button";
import AddReminder from "../screens/add-reminder";
import { useTabStore } from "../screens/editor/tiptap/use-tab-store";
import {
eSendEvent,
@@ -66,7 +69,6 @@ import { eUpdateNoteInEditor } from "../utils/events";
import { deleteItems } from "../utils/functions";
import { convertNoteToText } from "../utils/note-to-text";
import { sleep } from "../utils/time";
import AddReminder from "../screens/add-reminder";
export type ActionId =
| "select"
@@ -124,6 +126,7 @@ export type Action = {
hidden?: boolean;
activeColor?: string;
type?: ButtonProps["type"];
locked?: boolean;
};
function isNotePinnedInNotifications(item: Item) {
@@ -147,6 +150,15 @@ export const useActions = ({
close: () => void;
customActionHandlers?: Record<ActionId, () => void>;
}) => {
const features = useAreFeaturesAvailable([
"defaultNotebookAndTag",
"activeReminders",
"pinNoteInNotification",
"shortcuts",
"notebooks",
"customizableSidebar",
"customHomepage"
]);
const [item, setItem] = useState(propItem);
const { colors } = useThemeColors();
const setMenuPins = useMenuStore((state) => state.setMenuPins);
@@ -214,7 +226,7 @@ export const useActions = ({
async function restoreTrashItem() {
close();
if ((await db.trash.restore(item.id)) === false) return;
await db.trash.restore(item.id);
Navigation.queueRoutesForUpdate();
const type = item.type === "trash" ? item.itemType : item.type;
ToastManager.show({
@@ -246,6 +258,18 @@ export const useActions = ({
if (isPinnedToMenu) {
await db.shortcuts.remove(item.id);
} else {
if (features && !features?.shortcuts.isAllowed) {
ToastManager.show({
message: features?.shortcuts.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(features?.shortcuts);
}
});
return;
}
await db.shortcuts.add({
itemId: item.id,
itemType: item.type
@@ -415,12 +439,25 @@ export const useActions = ({
id: "reorder",
title: strings.reorder(),
icon: "sort-ascending",
onPress: () => {
onPress: async () => {
if (features && !features.customizableSidebar.isAllowed) {
ToastManager.show({
message: features.customizableSidebar.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(features.customizableSidebar);
}
});
return;
}
useSideBarDraggingStore.setState({
dragging: true
});
close();
}
},
locked: !features?.customizableSidebar.isAllowed
});
}
@@ -498,12 +535,26 @@ export const useActions = ({
await db.settings.setDefaultTag(undefined);
setDefaultTag(undefined);
} else {
if (features && !features.defaultNotebookAndTag.isAllowed) {
ToastManager.show({
message: features.defaultNotebookAndTag.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(features.defaultNotebookAndTag);
}
});
return;
}
await db.settings.setDefaultTag(item.id);
setDefaultTag(item.id);
}
close();
},
checked: defaultTag === item.id
checked: defaultTag === item.id,
locked: !features?.defaultNotebookAndTag.isAllowed
}
);
}
@@ -515,10 +566,24 @@ export const useActions = ({
title: strings.addNotebook(),
icon: "plus",
onPress: async () => {
if (features && !features.notebooks.isAllowed) {
ToastManager.show({
message: features.notebooks.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
ToastManager.hide();
PaywallSheet.present(features.notebooks);
}
});
return;
}
close();
await sleep(300);
AddNotebookSheet.present(undefined, item);
}
},
locked: !features?.notebooks.isAllowed
},
{
id: "edit-notebook",
@@ -543,6 +608,19 @@ export const useActions = ({
await db.settings.setDefaultNotebook(undefined);
setDefaultNotebook(undefined);
} else {
if (features && !features.defaultNotebookAndTag.isAllowed) {
ToastManager.show({
message: features.defaultNotebookAndTag.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(features.defaultNotebookAndTag);
}
});
return;
}
const notebook = {
id: item.id
};
@@ -551,7 +629,9 @@ export const useActions = ({
}
close();
},
checked: defaultNotebook === item.id
checked: defaultNotebook === item.id,
locked: !features?.defaultNotebookAndTag.isAllowed
},
{
id: "move-notes",
@@ -602,7 +682,20 @@ export const useActions = ({
icon: "home-outline",
isToggle: true,
checked: isHomepage,
onPress: () => {
onPress: async () => {
if (features && !features?.customHomepage.isAllowed) {
ToastManager.show({
message: features?.customHomepage.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(features?.customHomepage);
}
});
return;
}
SettingsService.setProperty(
"homepageV2",
isHomepage
@@ -684,6 +777,19 @@ export const useActions = ({
}
async function pinToNotifications() {
if (features && !features?.pinNoteInNotification.isAllowed) {
ToastManager.show({
message: features?.pinNoteInNotification.error,
type: "info",
actionText: strings.upgrade(),
context: "local",
func: () => {
PaywallSheet.present(features?.pinNoteInNotification);
}
});
return;
}
if (notifPinned) {
await Notifications.remove(item.id);
await sleep(500);
@@ -913,13 +1019,34 @@ export const useActions = ({
referenceType: "reminder",
relationType: "from",
title: strings.dataTypesPluralCamelCase.reminder(),
onAdd: () => {
onAdd: async () => {
if (features && !features.activeReminders.isAllowed) {
ToastManager.show({
type: "info",
message: features.activeReminders.error,
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(features.activeReminders);
}
});
}
AddReminder.present(undefined, item);
close();
},
button: {
type: "plain",
onPress: () => {
onPress: async () => {
if (features && !features.activeReminders.isAllowed) {
ToastManager.show({
type: "info",
message: features.activeReminders.error,
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(features.activeReminders);
}
});
return;
}
AddReminder.present(undefined, item);
close();
},
@@ -927,7 +1054,8 @@ export const useActions = ({
iconSize: 20
}
});
}
},
locked: !features?.activeReminders.isAllowed
},
{
@@ -1039,7 +1167,8 @@ export const useActions = ({
: strings.pinToNotifications(),
icon: "message-badge-outline",
checked: !!notifPinned,
onPress: pinToNotifications
onPress: pinToNotifications,
locked: !features?.pinNoteInNotification.isAllowed
});
}
}

View File

@@ -17,11 +17,13 @@ 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 { isFeatureAvailable } from "@notesnook/common";
import {
EV,
EVENTS,
EventManagerSubscription,
SYNC_CHECK_IDS,
SubscriptionPlan,
SyncStatusEvent,
User
} from "@notesnook/core";
@@ -49,7 +51,9 @@ import { MMKV } from "../common/database/mmkv";
import { endProgress, startProgress } from "../components/dialogs/progress";
import Migrate from "../components/sheets/migrate";
import NewFeature from "../components/sheets/new-feature";
import PaywallSheet from "../components/sheets/paywall";
import { Walkthrough } from "../components/walkthroughs";
import AddReminder from "../screens/add-reminder";
import {
resetTabStore,
useTabStore
@@ -102,7 +106,7 @@ import { getGithubVersion } from "../utils/github-version";
import { fluidTabsRef } from "../utils/global-refs";
import { NotesnookModule } from "../utils/notesnook-module";
import { sleep } from "../utils/time";
import AddReminder from "../screens/add-reminder";
import useFeatureManager from "./use-feature-manager";
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
const { disableSync, disableAutoSync } = SettingsService.get();
@@ -185,6 +189,18 @@ const onAppOpenedFromURL = async (event: { url: string }) => {
if (reminder) AddReminder.present(reminder);
}
} else if (url.startsWith("https://notesnook.com/new_reminder")) {
const reminderFeature = await isFeatureAvailable("activeReminders");
if (!reminderFeature.isAllowed) {
ToastManager.show({
type: "info",
message: reminderFeature.error,
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(reminderFeature);
}
});
return;
}
AddReminder.present();
}
} catch (e) {
@@ -209,12 +225,23 @@ const onUserEmailVerified = async () => {
const onUserSubscriptionStatusChanged = async (
subscription: User["subscription"]
) => {
if (!PremiumService.get() && subscription.type === 5) {
if (
subscription &&
subscription.plan !== SubscriptionPlan.FREE &&
subscription.plan !== useUserStore.getState().user?.subscription?.plan
) {
PremiumService.subscriptions.clear();
useUserStore.setState({
user: {
...(useUserStore.getState().user as User),
subscription: subscription
}
});
Walkthrough.present("prouser", false, true);
}
await PremiumService.setPremiumStatus();
useMessageStore.getState().setAnnouncement();
useUserStore.getState().setUser(await db.user.fetchUser());
};
const onRequestPartialSync = async (
@@ -298,6 +325,7 @@ async function saveEditorState() {
const onSuccessfulSubscription = async (
subscription: RNIap.ProductPurchase | RNIap.SubscriptionPurchase
) => {
if (Platform.OS === "android") return;
await PremiumService.subscriptions.set(subscription);
await PremiumService.subscriptions.verify(subscription);
};
@@ -318,6 +346,9 @@ const doAppLoadActions = async () => {
eSendEvent(eLoginSessionExpired);
return;
}
await useMessageStore.getState().setAnnouncement();
notifee.setBadgeCount(0);
if (!(await db.user.getUser())) {
@@ -325,11 +356,9 @@ const doAppLoadActions = async () => {
return;
}
await useMessageStore.getState().setAnnouncement();
if (NewFeature.present()) return;
if (await checkAppUpdateAvailable()) return;
if (await checkForRateAppRequest()) return;
if (await PremiumService.getRemainingTrialDaysStatus()) return;
if (SettingsService.get().introCompleted) {
useMessageStore.subscribe((state) => {
const dialogs = state.dialogs;
@@ -430,7 +459,7 @@ export const useAppEvents = () => {
state.appLocked,
state.syncing
]);
useFeatureManager();
const syncedOnLaunch = useRef(false);
const refValues = useRef<
Partial<{
@@ -606,7 +635,6 @@ export const useAppEvents = () => {
EV.subscribe(EVENTS.userLoggedOut, onLogout),
EV.subscribe(EVENTS.userEmailConfirmed, onUserEmailVerified),
EV.subscribe(EVENTS.userSessionExpired, onUserSessionExpired),
EV.subscribe(EVENTS.userCheckStatus, PremiumService.onUserStatusCheck),
EV.subscribe(
EVENTS.userSubscriptionUpdated,
onUserSubscriptionStatusChanged

View File

@@ -0,0 +1,105 @@
import { useAreFeaturesAvailable } from "@notesnook/common";
import { strings } from "@notesnook/intl";
import { useEffect } from "react";
import { db } from "../common/database";
import { presentDialog } from "../components/dialog/functions";
import { useDragState } from "../screens/settings/editor/state";
import { eSendEvent } from "../services/event-manager";
import Navigation from "../services/navigation";
import Notifications from "../services/notifications";
import SettingsService from "../services/settings";
import { useUserStore } from "../stores/use-user-store";
import { eCloseSimpleDialog } from "../utils/events";
export default function useFeatureManager() {
const features = useAreFeaturesAvailable([
"appLock",
"createNoteFromNotificationDrawer",
"pinNoteInNotification",
"markdownShortcuts",
"customHomepage",
"defaultNotebookAndTag",
"defaultSidebarTab",
"customToolbarPreset",
"disableTrashCleanup",
"fullOfflineMode"
]);
const user = useUserStore((state) => state.user);
const plan = useUserStore((state) => state.user?.subscription?.plan);
useEffect(() => {
if (!user || !features) return;
if (!features.createNoteFromNotificationDrawer.isAllowed) {
SettingsService.setProperty("notifNotes", false);
}
if (!features.pinNoteInNotification.isAllowed) {
Notifications.clearPinnedNotes();
}
if (!features.markdownShortcuts.isAllowed) {
SettingsService.setProperty("markdownShortcuts", false);
}
if (!features.customHomepage.isAllowed) {
SettingsService.setProperty("homepageV2", undefined);
}
if (!features.defaultSidebarTab.isAllowed) {
SettingsService.setProperty("defaultSidebarTab", 0);
}
if (
!features.customToolbarPreset.isAllowed &&
useDragState.getState().preset === "custom"
) {
useDragState.getState().setPreset("default");
}
if (!features.fullOfflineMode.isAllowed) {
SettingsService.setProperty("offlineMode", false);
}
if (!features.defaultNotebookAndTag.isAllowed) {
if (db.settings.getDefaultNotebook()) {
db.settings.setDefaultNotebook(undefined);
}
if (db.settings.getDefaultTag()) {
db.settings.setDefaultTag(undefined);
}
}
const isAppLocked = useUserStore.getState().appLocked;
let unsub: () => void;
if (isAppLocked) {
unsub = useUserStore.subscribe((state) => {
if (!state.appLocked && !features?.appLock?.isAllowed) {
unsub();
SettingsService.setProperty("appLockEnabled", false);
setTimeout(() => {
presentDialog({
title: "App Lock Disabled",
paragraph: features?.appLock?.error,
positiveText: strings.upgrade(),
negativeText: strings.cancel(),
positivePress: async () => {
eSendEvent(eCloseSimpleDialog);
if (SettingsService.getProperty("serverUrls")) return;
Navigation.navigate("PayWall", {
context: "logged-in"
});
}
});
}, 1000);
}
});
}
return () => {
unsub?.();
};
}, [features, plan]);
return true;
}

View File

@@ -0,0 +1,724 @@
/*
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, SubscriptionPlan, SubscriptionPlanId } from "@notesnook/core";
import { useEffect, useState } from "react";
import { Platform } from "react-native";
import Config from "react-native-config";
import * as RNIap from "react-native-iap";
import { DatabaseLogger, db } from "../common/database";
import PremiumService from "../services/premium";
import { useSettingStore } from "../stores/use-setting-store";
import { useUserStore } from "../stores/use-user-store";
import SettingsService from "../services/settings";
function numberWithCommas(x: string) {
const parts = x.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
}
export const PlanOverView = {
free: {
storage: `50 MB/mo`,
fileSize: `1 MB`,
hdImages: false
},
essential: {
storage: `1 GB`,
fileSize: `100 MB/mo`,
hdImages: false
},
pro: {
storage: `10 GB/mo`,
fileSize: `1 GB`,
hdImages: true
},
believer: {
storage: `25 GB/mo`,
fileSize: `5 GB`,
hdImages: true
}
};
export type PricingPlan = {
id: string;
name: string;
description: string;
subscriptionSkuList: string[];
subscriptions?: Record<string, RNIap.Subscription | undefined>;
products?: Record<string, RNIap.Product | undefined>;
trialSupported?: boolean;
recommended?: boolean;
productSkuList: string[];
};
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 pricingPlans: PricingPlan[] = [
{
id: "free",
name: "Free",
description: "Basic features for personal use",
subscriptionSkuList: [],
productSkuList: []
},
{
id: "essential",
name: "Essential",
description: "Unlocks essential features for personal use",
subscriptionSkuList: [
"notesnook.essential.monthly",
"notesnook.essential.yearly"
],
trialSupported: true,
productSkuList: []
},
{
id: "pro",
name: "Pro",
description: "Unlocks all features for professional use",
subscriptionSkuList: [
"notesnook.pro.monthly",
"notesnook.pro.yearly",
"notesnook.pro.yearly.tier2",
"notesnook.pro.yearly.tier3"
],
productSkuList: ["notesnook.pro.5year"],
trialSupported: true,
recommended: true
},
{
id: "believer",
name: "Believer",
description: "Become a believer and support the project",
subscriptionSkuList: [
"notesnook.believer.monthly",
"notesnook.believer.yearly"
],
productSkuList: ["notesnook.believer.5year"],
trialSupported: true
}
];
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"
};
type PricingPlansOptions = {
promoOffer?: {
promoCode: string;
};
planId?: string;
productId?: string;
onBuy?: () => void;
};
const planIdToIndex = (planId: string) => {
const planIndex = planId === "essential" ? 1 : planId === "pro" ? 2 : 3;
return planIndex;
};
let WebPlanCache: Plan[];
const usePricingPlans = (options?: PricingPlansOptions) => {
const isGithubRelease = Config.GITHUB_RELEASE === "true";
const user = useUserStore((state) => state.user);
const [currentPlan, setCurrentPlan] = useState<string>(
options?.planId || pricingPlans[2].id
);
const [plans, setPlans] = useState<PricingPlan[]>(pricingPlans);
const [loading, setLoading] = useState(false);
const [loadingPlans, setLoadingPlans] = useState(true);
const [selectedProductSku, setSelectedProductSku] = useState<string>(
options?.productId || "notesnook.pro.yearly"
);
const [isPromoOffer, setIsPromoOffer] = useState(false);
const [cancelPromo, setCancelPromo] = useState(false);
const [userCanRequestTrial, setUserCanRequestTrial] = useState(false);
const [webPricingPlans, setWebPricingPlans] = useState<Plan[]>([]);
const getProduct = (planId: string, skuId: string) => {
if (isGithubRelease)
return webPricingPlans.find(
(plan) => planIdToIndex(planId) === plan.plan && skuId === plan.period
);
return (
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
plans.find((p) => p.id === planId)?.products?.[skuId]
);
};
const getProductAndroid = (planId: string, skuId: string) => {
if (isGithubRelease)
return webPricingPlans.find(
(plan) => planIdToIndex(planId) === plan.plan && skuId === plan.period
);
return getProduct(planId, skuId) as RNIap.SubscriptionAndroid;
};
const getProductIOS = (planId: string, skuId: string) => {
return getProduct(planId, skuId) as RNIap.SubscriptionIOS;
};
const hasTrialOffer = (planId?: string, productId?: string) => {
if (!selectedProductSku && !productId) return false;
if (productId?.includes("5year")) return false;
if (isGithubRelease) {
if (
user?.subscription?.trialsAvailed?.some(
(plan) => plan === planIdToIndex(planId || currentPlan)
)
) {
return false;
} else {
return true;
}
}
return Platform.OS === "ios"
? (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionIOS
)?.introductoryPricePaymentModeIOS === "FREETRIAL"
: (
getProduct(
planId || currentPlan,
productId || selectedProductSku
) as RNIap.SubscriptionAndroid
)?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList
?.length > 1;
};
// user && (!user.subscription || !user.subscription.expiry) ? true : false;
useEffect(() => {
const loadPlans = async () => {
const items = await PremiumService.loadProductsAndSubs();
pricingPlans.forEach((plan) => {
plan.subscriptions = {};
plan.products = {};
plan.subscriptionSkuList.forEach((sku) => {
if (!plan.subscriptions) plan.subscriptions = {};
plan.subscriptions[sku] = items.subs.find((p) => p.productId === sku);
});
plan.productSkuList.forEach((sku) => {
if (!plan.products) plan.products = {};
plan.products[sku] = items.products.find((p) => p.productId === sku);
});
});
setPlans([...pricingPlans]);
setUserCanRequestTrial(hasTrialOffer());
if (
Config.GITHUB_RELEASE === "true" &&
!SettingsService.getProperty("serverUrls")
) {
try {
const products = WebPlanCache || (await db.pricing.products());
WebPlanCache = products;
setWebPricingPlans(products);
} catch (e) {}
}
setLoadingPlans(false);
};
loadPlans();
}, [options?.promoOffer, cancelPromo]);
function getLocalizedPrice(
product: RNIap.Subscription | RNIap.Product | Plan
) {
if (!product) return;
if (Platform.OS === "android") {
if (isGithubRelease) {
if (!(product as Plan)?.price) return null;
return `${
(product as Plan).currencySymbol || (product as Plan).currency
} ${
(product as Plan).period === "yearly"
? (product as Plan).price.gross
: (product as Plan).period === "5-year"
? (product as Plan).price.gross
: (product as Plan).price.gross
}`;
}
const pricingPhaseListItem =
(product as RNIap.SubscriptionAndroid)?.subscriptionOfferDetails?.[0]
?.pricingPhases?.pricingPhaseList?.[1] ||
(product as RNIap.SubscriptionAndroid)?.subscriptionOfferDetails?.[0]
?.pricingPhases?.pricingPhaseList?.[0];
return (
pricingPhaseListItem?.formattedPrice ||
(product as RNIap.ProductAndroid).oneTimePurchaseOfferDetails
?.formattedPrice
);
} else {
return (product as RNIap.SubscriptionIOS)?.localizedPrice;
}
}
function getOfferTokenAndroid(
product: RNIap.Subscription,
offerIndex: number
) {
return (product as RNIap.SubscriptionAndroid)?.subscriptionOfferDetails?.[
offerIndex
].offerToken;
}
async function subscribe(
product: RNIap.Subscription | RNIap.Product,
androidOfferToken?: string
) {
if (loading || !product || isGithubRelease) return;
setLoading(true);
try {
if (!user) {
setLoading(false);
return;
}
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
if (!product.productId.includes("5year")) {
if (Platform.OS === "android") {
androidOfferToken =
(
product as RNIap.SubscriptionAndroid
)?.subscriptionOfferDetails.find(
(offer) => offer.offerToken === androidOfferToken
)?.offerToken ||
(product as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails?.[0].offerToken;
if (!androidOfferToken) return;
}
DatabaseLogger.info(
`Subscription Requested initiated for user ${toUUID(user.id)}`
);
await RNIap.requestSubscription({
sku: product?.productId,
obfuscatedAccountIdAndroid: user.id,
obfuscatedProfileIdAndroid: user.id,
purchaseTokenAndroid:
user.subscription?.plan !== SubscriptionPlan.FREE
? user.subscription?.googlePurchaseToken || undefined
: undefined,
replacementModeAndroid: user.subscription?.googlePurchaseToken
? RNIap.ReplacementModesAndroid.WITH_TIME_PRORATION
: undefined,
/**
* iOS
*/
appAccountToken: toUUID(user.id),
andDangerouslyFinishTransactionAutomaticallyIOS: false,
subscriptionOffers: androidOfferToken
? [
{
offerToken: androidOfferToken,
sku: product?.productId
}
]
: undefined
});
if (
(product as RNIap.SubscriptionIOS).introductoryPricePaymentModeIOS ===
"FREETRIAL"
) {
PremiumService.subscriptions.setTrialStatus(true);
}
} else {
await RNIap.requestPurchase({
andDangerouslyFinishTransactionAutomaticallyIOS: false,
appAccountToken: toUUID(user.id),
obfuscatedAccountIdAndroid: user.id,
obfuscatedProfileIdAndroid: user.id,
sku: product.productId,
skus: [product.productId],
quantity: 1
});
}
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
setLoading(false);
options?.onBuy?.();
} catch (e) {
console.log(e);
setLoading(false);
}
}
function getPromoCycleText(product: RNIap.Subscription) {
if (!selectedProductSku) return;
const isMonthly = selectedProductSku?.indexOf(".monthly") > -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
];
return cycleText;
}
const getBillingPeriod = (
product: RNIap.Subscription | Plan,
offerIndex: number
) => {
if (isGithubRelease) {
return (product as Plan)?.period;
}
if (Platform.OS === "android") {
const period = (product as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails?.[offerIndex]?.pricingPhases
?.pricingPhaseList?.[0].billingPeriod;
return period.endsWith("W")
? "week"
: period.endsWith("M")
? "month"
: "year";
} else {
const unit = (product as RNIap.SubscriptionIOS)
?.subscriptionPeriodUnitIOS;
return unit?.toLocaleLowerCase();
}
};
const getBillingDuration = (
product: RNIap.Subscription | Plan,
offerIndex: number,
phaseIndex: number,
trialDurationIos?: boolean
) => {
if (!product) return;
if (isGithubRelease) {
return {
duration: 1,
type: (product as Plan)?.period
};
}
if ((product as RNIap.Subscription)?.productId.includes("5year")) {
return {
type: "year",
duration: 5
};
}
if (Platform.OS === "android") {
const phase = (product as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails?.[offerIndex]?.pricingPhases
?.pricingPhaseList?.[phaseIndex];
const duration = parseInt(phase.billingPeriod[1]);
return {
duration: phase.billingPeriod.endsWith("W") ? duration * 7 : duration,
type: phase.billingPeriod.endsWith("W")
? "week"
: phase.billingPeriod.endsWith("M")
? "month"
: "year"
};
} else {
const productIos = product as RNIap.SubscriptionIOS;
const unit = trialDurationIos
? productIos.introductoryPriceSubscriptionPeriodIOS
: productIos.subscriptionPeriodUnitIOS;
const duration = parseInt(
(trialDurationIos
? productIos.introductoryPriceNumberOfPeriodsIOS
: productIos.subscriptionPeriodNumberIOS) || "1"
);
return {
duration: unit === "WEEK" ? duration * 7 : duration,
type: unit?.toLocaleLowerCase()
};
}
};
const getTrialInfo = (product: RNIap.Subscription) => {
if (Platform.OS === "android") {
const ProductAndroid = (product as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails?.[0];
if (ProductAndroid.pricingPhases.pricingPhaseList?.length === 1) return;
return {
period:
ProductAndroid?.pricingPhases.pricingPhaseList?.[0].billingPeriod,
cycles:
ProductAndroid?.pricingPhases.pricingPhaseList?.[0].billingCycleCount
};
} else {
const productIos = product as RNIap.SubscriptionIOS;
if (!productIos.introductoryPrice) return;
return {
period: productIos.introductoryPriceSubscriptionPeriodIOS,
cycles: productIos.introductoryPriceNumberOfPeriodsIOS
? parseInt(productIos.introductoryPriceNumberOfPeriodsIOS as string)
: 1
};
}
};
const convertPrice = (
amount: number,
symbol: string,
isAtLeft: boolean,
splitBy = 12
) => {
const monthlyPrice = amount / splitBy;
const formattedPrice = numberWithCommas(monthlyPrice.toFixed(2));
return isAtLeft
? `${symbol} ${formattedPrice}`
: `${formattedPrice} ${symbol}`;
};
const getDiscountValue = (p1: string, p2: string, splitToMonth?: boolean) => {
let price1 = Platform.OS === "ios" ? parseInt(p1) : parseInt(p1) / 1000000;
const price2 =
Platform.OS === "ios" ? parseInt(p2) : parseInt(p2) / 1000000;
price1 = splitToMonth ? price1 / 12 : price1;
return (((price2 - price1) / price2) * 100).toFixed(0);
};
const compareProductPrice = (planId: string, sku1: string, sku2: string) => {
const plan = pricingPlans.find((p) => p.id === planId);
const p1 = plan?.subscriptions?.[sku1];
const p2 = plan?.subscriptions?.[sku2];
if (!p1 || !p2) return 0;
if (Platform.OS === "android") {
const androidPricingPhase1 =
(p1 as RNIap.SubscriptionAndroid)?.subscriptionOfferDetails?.[0]
.pricingPhases?.pricingPhaseList?.[1] ||
(p1 as RNIap.SubscriptionAndroid)?.subscriptionOfferDetails?.[0]
.pricingPhases?.pricingPhaseList?.[0];
const androidPricingPhase2 =
(p2 as RNIap.SubscriptionAndroid)?.subscriptionOfferDetails?.[0]
.pricingPhases?.pricingPhaseList?.[1] ||
(p2 as RNIap.SubscriptionAndroid)?.subscriptionOfferDetails?.[0]
.pricingPhases?.pricingPhaseList?.[0];
if (!androidPricingPhase1 || !androidPricingPhase2) return 0;
return getDiscountValue(
androidPricingPhase1.priceAmountMicros,
androidPricingPhase2.priceAmountMicros,
true
);
} else {
return getDiscountValue(
(p1 as RNIap.SubscriptionIOS).price,
(p2 as RNIap.SubscriptionIOS).price,
true
);
}
};
const getPriceParts = (price: number, localizedPrice: string) => {
let priceValue: number;
if (Platform.OS === "ios") {
priceValue = price;
} else {
priceValue = price / 1000000;
}
const priceSymbol = localizedPrice.replace(/[\s\d,.]+/, "");
return { priceValue, priceSymbol, localizedPrice };
};
const getPrice = (
product: RNIap.Subscription | RNIap.Product | Plan,
phaseIndex: number,
annualBilling?: boolean
) => {
if (!product) return null;
if (isGithubRelease) {
if (!(product as Plan)?.price) return null;
return `${
(product as Plan).currencySymbol || (product as Plan).currency
} ${
(product as Plan).period === "yearly"
? ((product as Plan).price.gross / 12).toFixed(2)
: (product as Plan).period === "5-year"
? ((product as Plan).price.gross / (12 * 5)).toFixed(2)
: (product as Plan).price.gross
}`;
}
const androidPricingPhase = (product as RNIap.SubscriptionAndroid)
?.subscriptionOfferDetails?.[0].pricingPhases?.pricingPhaseList?.[
phaseIndex
];
const { localizedPrice, priceSymbol, priceValue } = getPriceParts(
Platform.OS === "android"
? parseInt(
androidPricingPhase?.priceAmountMicros ||
(product as RNIap.ProductAndroid)?.oneTimePurchaseOfferDetails
?.priceAmountMicros ||
"0"
)
: parseInt((product as RNIap.SubscriptionIOS).price),
Platform.OS === "android"
? androidPricingPhase?.formattedPrice ||
(product as RNIap.ProductAndroid).oneTimePurchaseOfferDetails
?.formattedPrice ||
"0"
: (product as RNIap.SubscriptionIOS).localizedPrice
);
return !annualBilling &&
!(product as RNIap.Subscription)?.productId.includes("5year")
? getLocalizedPrice(product as RNIap.Subscription)
: convertPrice(
priceValue,
priceSymbol,
localizedPrice.startsWith(priceSymbol),
annualBilling ? 12 : 60
);
};
async function getRegionalDiscount(plan: string, productId: string) {
if (productId !== "notesnook.pro.yearly") {
return;
}
try {
return await db.pricing.sku(
Platform.OS === "android" ? "google" : "apple",
"yearly",
plan as SubscriptionPlanId
);
} catch (e) {
console.log(e);
}
}
return {
currentPlan: pricingPlans.find((p) => p.id === currentPlan),
pricingPlans: plans,
getStandardPrice: getLocalizedPrice,
loadingPlans,
loading,
selectPlan: (planId: string, productId?: string) => {
setCurrentPlan(planId);
if (productId) {
console.log(productId, "productId");
setSelectedProductSku(productId);
} else {
const product = plans.find((p) => p.id === planId)
?.subscriptionSkuList?.[0];
if (product) {
setSelectedProductSku(product);
}
}
setIsPromoOffer(false);
},
convertYearlyPriceToMonthly: convertPrice,
getOfferTokenAndroid,
subscribe,
selectProduct: setSelectedProductSku,
selectedProduct: selectedProductSku
? getProduct(currentPlan, selectedProductSku)
: undefined,
isPromoOffer,
getPromoCycleText,
getProduct,
getProductAndroid,
getProductIOS,
hasTrialOffer,
userCanRequestTrial: userCanRequestTrial,
cancelPromoOffer: () => setCancelPromo(true),
getBillingDuration,
getBillingPeriod,
getTrialInfo,
user,
getPrice,
compareProductPrice,
get5YearPlanProduct: () => {
if (currentPlan === "free" || currentPlan === "essential") return;
return plans.find((p) => p.id === "pro")?.products?.[
`notesnook.${currentPlan}.5year`
];
},
getWebPlan(plan: string, period: "monthly" | "yearly") {
const planIndex = planIdToIndex(plan);
return webPricingPlans.find(
(plan) => plan.plan === planIndex && plan.period === period
);
},
getRegionalDiscount,
isGithubRelease: isGithubRelease,
isSubscribed: () => user?.subscription?.plan !== SubscriptionPlan.FREE,
finish: () => options?.onBuy?.()
};
};
export default usePricingPlans;

View File

@@ -49,7 +49,9 @@ export const usePricing = (period: "monthly" | "yearly") => {
));
skuInfos[period] = skuInfo;
const products = (await PremiumService.getProducts()) as Subscription[];
const products = (await (
await PremiumService.loadProductsAndSubs()
).subs) as Subscription[];
let product = products.find((p) => p.productId === skuInfo?.sku);
if (!product)
product = products.find((p) => p.productId === getDefaultSku(period));

View File

@@ -31,6 +31,7 @@ import { useSelectionStore } from "../stores/use-selection-store";
import { useSettingStore } from "../stores/use-setting-store";
import { rootNavigatorRef } from "../utils/global-refs";
import Navigation from "../services/navigation";
import { useIsFeatureAvailable } from "@notesnook/common";
const RootStack = createNativeStackNavigator();
const AppStack = createNativeStackNavigator();
@@ -47,6 +48,7 @@ let ColoredNotes: any = null;
let Archive: any = null;
const AppNavigation = React.memo(
() => {
const customHomepageFeature = useIsFeatureAvailable("customHomepage");
const { colors } = useThemeColors();
const [home, setHome] = React.useState<{
name: string;
@@ -58,7 +60,7 @@ const AppNavigation = React.memo(
React.useEffect(() => {
(async () => {
if (loading) return;
if (!homepageV2) {
if (!homepageV2 || !customHomepageFeature?.isAllowed) {
setHome({
name: "Notes",
params: undefined
@@ -250,18 +252,20 @@ let MoveNotes: any = null;
let Settings: any = null;
let ManageTags: any = null;
let AddReminder: any = null;
let PayWall: any = null;
export const RootNavigation = () => {
const introCompleted = useSettingStore(
(state) => state.settings.introCompleted
);
const clearSelection = useSelectionStore((state) => state.clearSelection);
const resetTimer = React.useRef<NodeJS.Timeout>(undefined);
const onStateChange = React.useCallback(
(state: any) => {
if (useSelectionStore.getState().selectionMode) {
clearSelection();
}
setTimeout(() => {
clearTimeout(resetTimer.current);
resetTimer.current = setTimeout(() => {
Navigation.resetRootState(state);
}, 1000);
hideAllTooltips();
@@ -353,6 +357,13 @@ export const RootNavigation = () => {
return AddReminder;
}}
/>
<RootStack.Screen
name="PayWall"
getComponent={() => {
PayWall = PayWall || require("../components/paywall").default;
return PayWall;
}}
/>
</RootStack.Navigator>
</NavigationContainer>
);

View File

@@ -44,7 +44,8 @@
"@trpc/react-query": "^10.45.2",
"@trpc/server": "^10.45.2",
"@tanstack/react-query": "^4.36.1",
"async-mutex": "0.5.0"
"async-mutex": "0.5.0",
"react-async-hook": "^4.0.0"
},
"sideEffects": false
}

View File

@@ -47,7 +47,8 @@ import SettingsService from "../../services/settings";
import { useRelationStore } from "../../stores/use-relation-store";
import { AppFontSize, defaultBorderRadius } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { getFormattedDate } from "@notesnook/common";
import { getFormattedDate, useIsFeatureAvailable } from "@notesnook/common";
import PaywallSheet from "../../components/sheets/paywall";
const ReminderModes =
Platform.OS === "ios"
@@ -107,6 +108,7 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
const [repeatFrequency, setRepeatFrequency] = useState(1);
const referencedItem = reference ? (reference as Note) : null;
const recurringReminderFeature = useIsFeatureAvailable("recurringReminders");
const title = useRef<string | undefined>(
!reminder ? referencedItem?.title : reminder?.title
@@ -304,7 +306,14 @@ export default function AddReminder(props: NavigationProps<"AddReminder">) {
: "plain"
}
onPress={() => {
if (mode === "Repeat" && !PremiumService.get()) return;
if (
recurringReminderFeature &&
!recurringReminderFeature?.isAllowed
) {
PaywallSheet.present(recurringReminderFeature);
return;
}
setReminderMode(
ReminderModes[
mode as keyof typeof ReminderModes

View File

@@ -47,7 +47,6 @@ export const Archive = ({ navigation, route }: NavigationProps<"Archive">) => {
return (
<>
<SelectionHeader id={route.name} items={archive} type="note" />
<Header
renderedInRoute={route.name}
title={strings.routes[route.name]()}
@@ -81,6 +80,8 @@ export const Archive = ({ navigation, route }: NavigationProps<"Archive">) => {
headerTitle={strings.routes.Archive()}
/>
</DelayLayout>
<SelectionHeader id={route.name} items={archive} type="note" />
</>
);
};

View File

@@ -18,7 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Sodium from "@ammarahmed/react-native-sodium";
import { isFeatureAvailable } from "@notesnook/common";
import { isImage } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { basename } from "pathe";
import { Platform } from "react-native";
import RNFetchBlob from "react-native-blob-util";
@@ -38,12 +40,10 @@ import {
} from "../../../services/event-manager";
import PremiumService from "../../../services/premium";
import { useSettingStore } from "../../../stores/use-setting-store";
import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
import { useUserStore } from "../../../stores/use-user-store";
import { eCloseSheet } from "../../../utils/events";
import { useTabStore } from "./use-tab-store";
import { editorController, editorState } from "./utils";
import { strings } from "@notesnook/intl";
import { useUserStore } from "../../../stores/use-user-store";
const showEncryptionSheet = (file: DocumentPickerResponse) => {
presentSheet({
@@ -92,13 +92,13 @@ const file = async (fileOptions: PickerOptions) => {
let uri = Platform.OS === "ios" ? file.fileCopyUri || file.uri : file.uri;
if ((file.size || 0) > FILE_SIZE_LIMIT) {
const featureResult = await isFeatureAvailable("fileSize", file.size || 0);
if (!featureResult.isAllowed) {
ToastManager.show({
heading: strings.fileTooLarge(),
message: strings.fileTooLargeDesc(500),
message: featureResult.error,
type: "error"
});
return;
}
if (file.copyError) {
@@ -234,25 +234,22 @@ const gallery = async (options: PickerOptions) => {
};
const pick = async (options: PickerOptions) => {
if (!PremiumService.get()) {
const user = await db.user.getUser();
if (!user) {
ToastManager.show({
heading: strings.loginRequired(),
type: "error"
});
return;
}
if (editorState().isFocused) {
editorState().isFocused = true;
}
if (user && !PremiumService.get() && !user?.isEmailConfirmed) {
PremiumService.showVerifyEmailDialog();
} else {
PremiumService.sheet();
}
const user = await db.user.getUser();
if (!user) {
ToastManager.show({
heading: strings.loginRequired(),
type: "error"
});
return;
}
if (editorState().isFocused) {
editorState().isFocused = true;
}
if (user && !user?.isEmailConfirmed) {
PremiumService.showVerifyEmailDialog();
return;
}
useUserStore.getState().setDisableAppLockRequests(true);
if (options?.type.startsWith("image") || options?.type === "camera") {
if (options.type.startsWith("image")) {
@@ -290,14 +287,15 @@ const handleImageResponse = async (
Platform.OS === "ios" ? image.path.replace("file://", "") : image.path;
}
if (image.size > IMAGE_SIZE_LIMIT) {
const featureResult = await isFeatureAvailable("fileSize", image.size || 0);
if (!featureResult.isAllowed) {
ToastManager.show({
heading: strings.fileTooLarge(),
message: strings.fileTooLargeDesc(50),
message: featureResult.error,
type: "error"
});
return;
}
const b64 = `data:${image.mime};base64, ` + image.data;
const uri = decodeURI(image.path);
const hash = await Sodium.hashFile({

View File

@@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import type { ToolbarGroupDefinition } from "@notesnook/editor";
import { useEditor } from "./use-editor";
import { FeatureId, FeatureResult } from "@notesnook/common";
export type useEditorType = ReturnType<typeof useEditor>;
export type EditorState = {
@@ -57,6 +58,7 @@ export type Settings = {
timeFormat: string;
fontScale: number;
markdownShortcuts: boolean;
features: Record<any, any>;
};
export type EditorProps = {

View File

@@ -80,6 +80,8 @@ import { EditorMessage, EditorProps, useEditorType } from "./types";
import { useTabStore } from "./use-tab-store";
import { editorState, openInternalLink } from "./utils";
import AddReminder from "../../add-reminder";
import { isFeatureAvailable, useAreFeaturesAvailable } from "@notesnook/common";
import PaywallSheet from "../../../components/sheets/paywall";
const publishNote = async () => {
const user = useUserStore.getState().user;
@@ -148,6 +150,11 @@ export const useEditorEvents = (
editor: useEditorType,
{ readonly: editorPropReadonly, noHeader, noToolbar }: Partial<EditorProps>
) => {
const features = useAreFeaturesAvailable([
"callout",
"outlineList",
"taskList"
]);
const deviceMode = useSettingStore((state) => state.deviceMode);
const fullscreen = useSettingStore((state) => state.fullscreen);
const corsProxy = useSettingStore((state) => state.settings.corsProxy);
@@ -157,7 +164,6 @@ export const useEditorEvents = (
state.timeFormat
]);
const handleBack = useRef<NativeEventSubscription>();
const isPremium = useUserStore((state) => state.premium);
const { fontScale } = useWindowDimensions();
const doubleSpacedLines = useSettingStore(
@@ -201,7 +207,7 @@ export const useEditorEvents = (
editor.commands.setSettings({
deviceMode: deviceMode || "mobile",
fullscreen: fullscreen || false,
premium: isPremium,
premium: false,
readonly: false,
tools: tools || getDefaultPresets().default,
noHeader: noHeader,
@@ -216,11 +222,11 @@ export const useEditorEvents = (
dateFormat: db.settings?.getDateFormat(),
timeFormat: db.settings?.getTimeFormat(),
fontScale,
markdownShortcuts
markdownShortcuts,
features
});
}, [
fullscreen,
isPremium,
editor.loading,
deviceMode,
tools,
@@ -437,7 +443,23 @@ export const useEditorEvents = (
referenceType: "reminder",
relationType: "from",
title: strings.dataTypesPluralCamelCase.reminder(),
onAdd: () => AddReminder.present(undefined, note)
onAdd: async () => {
const reminderFeature = await isFeatureAvailable(
"activeReminders"
);
if (!reminderFeature.isAllowed) {
ToastManager.show({
type: "info",
message: reminderFeature.error,
actionText: strings.upgrade(),
func: () => {
PaywallSheet.present(reminderFeature);
}
});
return;
}
AddReminder.present(undefined, note);
}
});
break;
case EditorEvents.newtag:
@@ -525,7 +547,9 @@ export const useEditorEvents = (
if (editor.state.current?.isFocused) {
editor.state.current.isFocused = true;
}
eSendEvent(eOpenPremiumDialog);
PaywallSheet.present(
await isFeatureAvailable(editorMessage.value.feature)
);
break;
case EditorEvents.monograph:
publishNote();

View File

@@ -28,7 +28,6 @@ import {
View
} from "react-native";
import Editor from ".";
import { PremiumToast } from "../../components/premium/premium-toast";
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
import useIsFloatingKeyboard from "../../hooks/use-is-floating-keyboard";
import useKeyboard from "../../hooks/use-keyboard";
@@ -112,7 +111,6 @@ export const EditorWrapper = ({ widths }: { widths: any }) => {
enabled={!floating}
keyboardVerticalOffset={0}
>
<PremiumToast key="toast" context="editor" offset={50 + insets.top} />
<TextInput
key="input"
ref={textInput}

View File

@@ -50,7 +50,6 @@ export const Favorites = ({
return (
<>
<SelectionHeader id={route.name} items={favorites} type="note" />
<Header
renderedInRoute={route.name}
title={strings.routes[route.name]()}
@@ -84,6 +83,8 @@ export const Favorites = ({
headerTitle={strings.routes.Favorites()}
/>
</DelayLayout>
<SelectionHeader id={route.name} items={favorites} type="note" />
</>
);
};

View File

@@ -49,7 +49,6 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
return (
<>
<SelectionHeader id={route.name} items={notes} type="note" />
<Header
renderedInRoute={route.name}
title={strings.routes[route.name]()}
@@ -86,6 +85,7 @@ export const Home = ({ navigation, route }: NavigationProps<"Notes">) => {
<FloatingButton onPress={openEditor} alwaysVisible />
)}
</DelayLayout>
<SelectionHeader id={route.name} items={notes} type="note" />
</>
);
};

View File

@@ -44,6 +44,9 @@ import Paragraph from "../../components/ui/typography/paragraph";
import { AddNotebookSheet } from "../../components/sheets/add-notebook";
import { Notice } from "../../components/ui/notice";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { isFeatureAvailable } from "@notesnook/common";
import { ToastManager } from "../../services/event-manager";
import PaywallSheet from "../../components/sheets/paywall";
const {
useNotebookExpandedStore,
@@ -233,7 +236,23 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
}}
button={{
icon: "plus",
onPress: () => {
onPress: async () => {
const notebooksFeature = await isFeatureAvailable(
"notebooks"
);
if (!notebooksFeature.isAllowed) {
ToastManager.show({
message: notebooksFeature.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
ToastManager.hide();
PaywallSheet.present(notebooksFeature);
}
});
return;
}
AddNotebookSheet.present(
undefined,
undefined,
@@ -281,7 +300,12 @@ const LinkNotebooks = (props: NavigationProps<"LinkNotebooks">) => {
/>
{hasSelection ? (
<FloatingButton icon="check" alwaysVisible onPress={() => onSave()} />
<FloatingButton
testID="floating-save-button"
icon="check"
alwaysVisible
onPress={() => onSave()}
/>
) : null}
</SafeAreaView>
);
@@ -376,7 +400,21 @@ const NotebookItemWrapper = React.memo(
disableExpand={disableExpand}
focused={false}
onPress={onPress}
onAddNotebook={() => {
onAddNotebook={async () => {
const notebooksFeature = await isFeatureAvailable("notebooks");
if (!notebooksFeature.isAllowed) {
ToastManager.show({
message: notebooksFeature.error,
type: "info",
context: "local",
actionText: strings.upgrade(),
func: () => {
ToastManager.hide();
PaywallSheet.present(notebooksFeature);
}
});
return;
}
AddNotebookSheet.present(
undefined,
item.notebook,

View File

@@ -53,6 +53,7 @@ import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../utils/styles";
import { Header } from "../../components/header";
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
import { isFeatureAvailable } from "@notesnook/common";
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
@@ -152,11 +153,22 @@ const ManageTags = (props: NavigationProps<"ManageTags">) => {
v.and([v(`title`, "==", tag)])
);
const id = exists
? exists?.id
: await db.tags.add({
title: tag
let id = exists?.id;
if (!id) {
const tagsFeature = await isFeatureAvailable("tags");
if (!tagsFeature.isAllowed) {
ToastManager.show({
message: tagsFeature.error,
type: "info",
context: "local"
});
return;
}
id = await db.tags.add({
title: tag
});
}
if (id) {
for (const noteId of ids) {

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