Compare commits

...

517 Commits

Author SHA1 Message Date
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
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
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
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
Ammar Ahmed
9470d2b61f mobile: allow underscores in url validation 2025-09-25 12:19:16 +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
Ammar Ahmed
5e05d96487 Merge pull request #8588 from streetwriters/mobile-release-3.2.12
Mobile release 3.2.12
2025-09-19 10:41:45 +05:00
Ammar Ahmed
18c9912068 mobile: update changelog 2025-09-19 09:55:36 +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
Ammar Ahmed
bdb135036c mobile: release v3.2.12 2025-09-18 12:43:30 +05:00
Ammar Ahmed
5d70f58a0d mobile: fix editor styles 2025-09-18 12:42:40 +05:00
Ammar Ahmed
b7d2d66e82 mobile: fix add tag button not show when no tags 2025-09-18 12:42:28 +05:00
Ammar Ahmed
6b028ef850 mobile: fix localization 2025-09-18 12:42:18 +05:00
Ammar Ahmed
8f85795e53 mobile: fix notebook tree in notesnook sheet 2025-09-18 12:42:07 +05:00
Ammar Ahmed
a0b7dff5fd Merge pull request #8585 from streetwriters/mobile-release-3.2.11
Mobile release 3.2.11
2025-09-18 12:39:21 +05:00
Ammar Ahmed
352018c042 Merge branch 'master' into mobile-release-3.2.11
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-09-18 12:39:10 +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
Abdullah Atta
fd48684396 Merge pull request #8568 from 01zulfi/web/attachment-navigate
web: fix settings not opening again after navigation from attachment manager
2025-09-16 13:03:25 +05:00
01zulfi
9b25ab8708 web: fix settings not opening again after navigation from attachment manager
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-16 12:37:36 +05:00
Abdullah Atta
c725ba8530 Merge pull request #8574 from streetwriters/ci/fix-desktop-tests-not-running
Fix desktop tests not triggering on PR/commit
2025-09-16 11:15:41 +05:00
Abdullah Atta
98287fbfc4 ci: fix desktop tests not triggering on PR/commit 2025-09-16 11:07:52 +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
9f24bd2250 Merge pull request #8573 from streetwriters/intl-fix-strings
intl: fix strings.ts
2025-09-16 10:12:13 +05:00
Ammar Ahmed
94f2a216ad intl: fix strings.ts 2025-09-16 10:11:43 +05:00
Ammar Ahmed
aeb4c984eb Merge branch 'fix-ipad-split-view' into beta 2025-09-16 10:10:17 +05:00
Ammar Ahmed
78e3ee5fd2 Merge pull request #8572 from streetwriters/fix-ipad-split-view
Fix keyboard in iPad split view
2025-09-16 10:10:05 +05:00
Ammar Ahmed
e94a2b2aa1 Merge branch 'mobile-editor-ui-update' into beta 2025-09-16 10:09:49 +05:00
Ammar Ahmed
1ddcf44eb0 Merge pull request #8551 from streetwriters/mobile-editor-ui-update
mobile: editor ui overhaul
2025-09-16 10:08:28 +05:00
Ammar Ahmed
5c5fb2ba2c Merge branch 'master' into mobile-editor-ui-update
Signed-off-by: Ammar Ahmed <40239442+ammarahm-ed@users.noreply.github.com>
2025-09-16 10:08:10 +05:00
Ammar Ahmed
9344eadf34 Merge branch 'mobile-reminder-fixes' into beta 2025-09-16 10:06:58 +05:00
Ammar Ahmed
aa4995d871 Merge pull request #8525 from streetwriters/mobile-reminder-fixes
Fix reminders on mobile
2025-09-16 10:06:54 +05:00
Ammar Ahmed
e40c42afc3 Merge branch 'mobile-fix-add-notebook-btn' into beta 2025-09-16 10:06:34 +05:00
Ammar Ahmed
f818c41a02 Merge pull request #8524 from streetwriters/mobile-fix-add-notebook-btn
Fix add notebook button in Notebooks sheet
2025-09-16 10:06:30 +05:00
Ammar Ahmed
178d7e7471 Merge branch 'mobile-fix-notebook-tree' into beta 2025-09-16 10:06:11 +05:00
Ammar Ahmed
3f9d954e2f Merge pull request #8508 from streetwriters/mobile-fix-notebook-tree
Fix Notebook Tree when Searching for Notebooks
2025-09-16 10:06:05 +05:00
Ammar Ahmed
48a3eee232 Merge branch 'mobile-fix-reset-navigation' into beta 2025-09-16 10:05:00 +05:00
Ammar Ahmed
f1fcd227e7 Merge pull request #8535 from streetwriters/mobile-fix-reset-navigation
mobile: fix resetting navigation
2025-09-16 10:04:54 +05:00
Ammar Ahmed
84af1a9411 Merge branch 'mobile-fix-string-email' into beta 2025-09-16 10:04:05 +05:00
Ammar Ahmed
44d910121c Merge pull request #8507 from streetwriters/mobile-fix-string-email
Fix email confirmed description
2025-09-16 10:03:40 +05:00
Abdullah Atta
1258a95aad Merge pull request #8562 from streetwriters/fix/refresh-note-on-attachment-delete
Refresh note on attachment permanent delete
2025-09-16 09:43:15 +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
Ammar Ahmed
1379193065 mobile: fix keyboard in ipad split view 2025-09-16 09:11:34 +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
a7fa42e944 web: refresh note on attachment permanent delete 2025-09-13 13:07:02 +05:00
Ammar Ahmed
02ab8e3938 mobile: editor ui improvements 2025-09-12 10:57:18 +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
5c120676e1 Merge pull request #8550 from streetwriters/release/web/v3.2.4
Release v3.2.4
2025-09-12 08:47:48 +05:00
Abdullah Atta
3821dc29ef web: fix some editor tests on macos 2025-09-12 08:28:39 +05:00
Abdullah Atta
3c51f73427 web: fix cannot destructure property checkout of He as it is undefined 2025-09-12 08:28:39 +05:00
Abdullah Atta
8db373ee34 web: bump version to 3.2.4 2025-09-12 08:28:39 +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
adcb73a2fc Merge pull request #8388 from streetwriters/fix/theme-issues-2
Fix theme related issues
2025-09-12 08:27:44 +05:00
Ammar Ahmed
4c97b3481d mobile: editor ui overhaul 2025-09-11 15:49:00 +05:00
Abdullah Atta
38579c37e1 Merge pull request #8549 from 01zulfi/web/fix-toc-crash 2025-09-11 13:49:32 +05:00
Abdullah Atta
4785544e18 Merge pull request #8549 from 01zulfi/web/fix-toc-crash
web: fix crash when shift+click on toc item
2025-09-11 13:48:33 +05:00
01zulfi
7812c15b48 web: fix crash when shift+click on toc item
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-11 09:39:00 +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
b841c6e0eb Merge pull request #8395 from 01zulfi/editor/image-resizing-dimensions
editor: show image dimensions during drag resizing
2025-09-09 14:51:44 +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
55b6be0f15 Merge pull request #8542 from 01zulfi/editor/improve-insert-link-dialog
editor: if selected text is link, autofill url field in insert link popup
2025-09-09 13:07:18 +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
dfe6a73ac1 Merge pull request #8546 from streetwriters/release/monograph/1.2.5
Release monographs v1.2.5
2025-09-09 12:53:20 +05:00
Abdullah Atta
afcb8835d2 ci: do not build for armv7 & armv8 platforms 2025-09-09 12:49:33 +05:00
Abdullah Atta
9dc9f186e1 monograph: use oven/bun:1.2.21-slim 2025-09-09 12:45:15 +05:00
Ammar Ahmed
875125cfe4 mobile: clear logs 2025-09-09 12:34:06 +05:00
Abdullah Atta
32d9cdd265 monograph: bump version to 1.2.5 2025-09-09 12:00:28 +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
09deae9dbf Merge pull request #8545 from streetwriters/fix-monograph-report-not-working
Add report endpoint to connect-src content policy
2025-09-09 11:59:02 +05:00
Abdullah Atta
337582ac70 monograph: add report endpoint to connect-src content policy 2025-09-09 11:58:29 +05:00
01zulfi
820c78ce81 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-08 11:28:16 +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
Ammar Ahmed
565e692af8 mobile: fix crash during navigation 2025-09-05 12:40:20 +05:00
Ammar Ahmed
94e4721663 mobile: fix resetting navigation 2025-09-05 12:11:39 +05:00
Ammar Ahmed
8157c0812a mobile: reminder fixes
1. Allow editing reminder without changing date
2. Fix colors of buttons
3. Allow picking date and time in date picker dialog
4. Fix auto focus of title and description inputs
2025-09-05 11:30:18 +05:00
Ammar Ahmed
bc36837cd0 mobile: add missing buttons in reminder sheet 2025-09-04 14:26:47 +05:00
Ammar Ahmed
33540b000c mobile: show reminder description in properties 2025-09-04 14:26:33 +05:00
Ammar Ahmed
5a63a45d00 mobile: fix add notebook button 2025-09-04 11:43:36 +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
01zulfi
8fce6b5eb6 editor: show image dimensions during drag resizing
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-09-02 10:18:36 +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
4e44661e1b Merge pull request #8487 from 01zulfi/web/reminder-search-delete
web: fix deleting a reminder not removing from search list
2025-08-29 11:43:15 +05:00
Abdullah Atta
827ca2e427 Merge pull request #8509 from 01zulfi/editor/handle-empty-link-hrefs 2025-08-29 11:42:16 +05:00
Abdullah Atta
81fd1c097e Merge pull request #8509 from 01zulfi/editor/handle-empty-link-hrefs
editor: don't show open & copy tools for empty links
2025-08-29 11:39:39 +05:00
01zulfi
aaf82f359b editor: don't show open & copy tools for empty links
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-08-28 11:45:12 +05:00
Ammar Ahmed
aa7f14ae79 mobile: fix email confirmed description 2025-08-28 10:12:32 +05:00
Ammar Ahmed
0ad63da4a2 mobile: fix tree items in search 2025-08-25 14:51:56 +05:00
Ammar Ahmed
2885b9ac3e mobile: fix notebook tree when searching notebooks 2025-08-25 13:08:29 +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
a15ab99056 mobile: release v3.2.11 2025-08-20 10:32:54 +05:00
Ammar Ahmed
222cdbfa63 mobile: fix navigation 2025-08-20 10:01:19 +05:00
01zulfi
a1d8b34fb4 web: fix deleting a reminder not removing from search list
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-08-20 09:02:44 +05:00
Ammar Ahmed
35d08a6dfa Merge branch 'mobile-fix-open-note-notification' into beta 2025-08-20 08:49:59 +05:00
Ammar Ahmed
874352e753 Merge pull request #8400 from streetwriters/mobile-fix-open-note-notification
Fix opening pinned note from notification on android
2025-08-20 08:49:55 +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
49d1ab4be5 Merge pull request #8401 from streetwriters/mobile-fix-notebook-tag-sorting-filter
Fix filtered tags and notebooks not sorted correctly
2025-08-20 08:49:21 +05:00
Ammar Ahmed
54a0d0fd0c Merge branch 'mobile-fix-back-navigation' into beta 2025-08-20 08:48:58 +05:00
Ammar Ahmed
65a80dbd46 Merge pull request #8402 from streetwriters/mobile-fix-back-navigation
Fix back navigation on first app launch by resetting routes
2025-08-20 08:48:40 +05:00
Ammar Ahmed
dfeb01320d Merge branch 'fix-cache-clear-error' into beta 2025-08-20 08:48:20 +05:00
Ammar Ahmed
e55456f492 Merge pull request #8467 from streetwriters/fix-cache-clear-error
mobile: fix error when clearing cache on android
2025-08-20 08:48:01 +05:00
Ammar Ahmed
d33209afcc Merge branch 'fix-open-share-exported-notes-ios' into beta 2025-08-20 08:47:40 +05:00
Ammar Ahmed
3f9fce0d34 Merge pull request #8482 from streetwriters/fix-open-share-exported-notes-ios
mobile: fix sharing and opening exported notes on iOS
2025-08-20 08:47:21 +05:00
Ammar Ahmed
2ae094cf1f mobile: fix sharing and opening exported notes 2025-08-19 12:51:16 +05:00
Abdullah Atta
5530270b06 Merge pull request #8466 from 01zulfi/web/list-pane-max-size 2025-08-19 09:53:06 +05:00
Abdullah Atta
bc224f1674 Merge pull request #8466 from 01zulfi/web/list-pane-max-size
web: increase list pane max size to 1000 for large screens
2025-08-19 09:52:28 +05:00
01zulfi
8bb1582cca web: increase list pane max size to 1000
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-08-18 15:48:59 +05:00
01zulfi
c759408fdc core: add support for monographs sync 2025-08-18 13:16:12 +05:00
Ammar Ahmed
d02125a93c mobile: fix error when clearing cache on android 2025-08-18 13:15:45 +05:00
Abdullah Atta
30553ed0f1 Merge pull request #8389 from 01zulfi/common/update-keybindings 2025-08-13 13:31:34 +05:00
Abdullah Atta
d91ce4e936 Merge pull request #8389 from 01zulfi/common/update-keybindings
common: update keybindings for editor search, link, internal link, and command palette
2025-08-13 13:30:39 +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
deb902549b Merge pull request #8392 from 01zulfi/web/fix-open-settings-shortcut
web: fix open settings shortcut interfering with editor shortcut
2025-08-13 13:29:34 +05:00
Abdullah Atta
d6c6db68ba Merge pull request #8385 from Scott-will/master 2025-08-13 13:27:51 +05:00
Abdullah Atta
e41ed07805 Merge pull request #8385 from Scott-will/master
Handle pasting urls in html
2025-08-13 13:27:14 +05:00
Scott-will
f6e8dc0a88 editor: fix pasting html links with titles that look like links
Signed-off-by: Scott Williams scottwill1999@gmail.com

editor: update tests for html handling

Signed-off-by: Scott Williams scottwill1999@gmail.com

editor: add logic for markdown links

Signed-off-by: Scott Williams scottwill1999@gmail.com

editor: remove plugin in favour of markPasteRule

Signed-off-by: Scott Williams scottwill1999@gmail.com
2025-08-13 13:26:01 +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
Abdullah Atta
044dd5ce59 Merge pull request #8391 from 01zulfi/docs/update-desktop-instructions
docs: add missing instruction in desktop app dev setup
2025-08-13 12:21:12 +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
b24830666c Merge pull request #8409 from streetwriters/fix-fdroid-build
mobile: fix fdroid build
2025-07-31 09:10:13 +05:00
Ammar Ahmed
470802c3e2 mobile: fix fdroid build 2025-07-31 09:09:38 +05:00
Ammar Ahmed
0e869708f7 Merge branch 'mobile-fix-fdroid-build' into beta 2025-07-29 15:05:53 +05:00
Ammar Ahmed
7e0e1eac93 Merge pull request #8406 from streetwriters/mobile-fix-fdroid-build
mobile: compile lazysodium from source
2025-07-29 15:05:39 +05:00
Ammar Ahmed
58cbf653b2 mobile: compile lazysodium from source 2025-07-29 15:04:49 +05:00
Ammar Ahmed
4b35f2ecd2 mobile: fix back navigation on first app launch 2025-07-28 14:20:11 +05:00
Ammar Ahmed
48d9a863ec mobile: fix filtered tags and notebooks not sorted correctly 2025-07-28 13:42:46 +05:00
Ammar Ahmed
1d616cf68f mobile: fix opening note from notification on android 2025-07-28 11:56:54 +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
cdf6a4bb5b Merge pull request #8398 from streetwriters/mobile-release-3.2.10-fix
mobile: release v3.2.10
2025-07-26 12:48:28 +05:00
Ammar Ahmed
1b3735e2b2 mobile: release v3.2.10 2025-07-26 12:19:13 +05:00
Ammar Ahmed
54008a0de2 Merge branch 'mobile-release-3.2.9' into beta 2025-07-26 11:58:43 +05:00
Ammar Ahmed
850fc6b066 Merge pull request #8397 from streetwriters/mobile-release-3.2.9
mobile: release v3.2.9
2025-07-26 11:58:20 +05:00
Ammar Ahmed
67a398f583 Merge branch 'mobile-editor-ux-improvement' into beta 2025-07-26 10:53:01 +05:00
Ammar Ahmed
8a62070c8e Merge pull request #8394 from streetwriters/mobile-editor-ux-improvement
Mobile editor ux improvement
2025-07-26 10:52:01 +05:00
Ammar Ahmed
8aa31b8380 mobile: fix exiting table not working 2025-07-25 14:24:14 +05:00
Ammar Ahmed
f445a963f9 mobile: improve ux when clicking empty area in editor 2025-07-25 12:44:55 +05:00
Ammar Ahmed
d6e3f27612 mobile: add scroll to top and bottom in editor 2025-07-25 12:44:34 +05:00
Ammar Ahmed
4688d74edb mobile: improvements to editor ux
1. Tapping on empty in editor opens the keyboard
2. Swiping to open the editor when editor is empty opens the keyboard automatically
2025-07-25 11:57:44 +05:00
Ammar Ahmed
e88c755ba8 Merge branch 'mobile-fix-edit-notebook' into beta 2025-07-25 11:01:20 +05:00
Ammar Ahmed
1985007039 Merge pull request #8393 from streetwriters/mobile-fix-edit-notebook
mobile: fix edit notebook button causes app freeze on iOS
2025-07-25 11:01:03 +05:00
Ammar Ahmed
039c8faabe mobile: fix edit notebook button causes app freeze 2025-07-25 10:57:14 +05:00
01zulfi
fb1eb234b6 web: fix open settings shortcut interfering with editor shortcut
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-25 08:25:01 +05:00
01zulfi
7e70cd96b9 docs: add missing instruction in desktop app dev setup
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-24 15:56:17 +05:00
01zulfi
f9a89783ed common: update keybindings for editor search, link, internal link, and command palette
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-23 09:45:12 +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
7f3c74b55b editor: fix theme related issues 2025-07-23 08:32:35 +05:00
Abdullah Atta
301ac781d6 Merge pull request #8387 from 01zulfi/docs/keyboard-shortcuts-heading
docs: add keyboard shortcuts heading
2025-07-23 08:31:41 +05:00
01zulfi
ce3c98ae9b docs: add keyboard shortcuts heading
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 16:28:07 +05:00
Abdullah Atta
1c3a6ffbde Merge pull request #7655 from 01zulfi/global/keyboard 2025-07-22 14:22:22 +05:00
Abdullah Atta
2447b3bea7 Merge pull request #7655 from 01zulfi/global/keyboard
global: keyboard shortcuts documentation && web dialog
2025-07-22 14:21:30 +05:00
Abdullah Atta
63888ff70e web: improve keyboard shortcuts dialog UI 2025-07-22 14:20:15 +05:00
Abdullah Atta
c6e3d1bd47 common: New note should be in the General category 2025-07-22 14:20:15 +05:00
Abdullah Atta
e03e02d702 common: add license header to keybindings.ts 2025-07-22 14:20:15 +05:00
Abdullah Atta
1abf51ae2b common: getGroupedKeybindings should return an array
this is so the groups are always ordered. It also makes it easier to iterate
over the groups.
2025-07-22 14:20:15 +05:00
Abdullah Atta
c41dfa3185 common: letter keys should be upper cased
this is common in a lot of places and it provides better readability as well.
2025-07-22 14:20:15 +05:00
01zulfi
52a07da230 editor: remove unneeded code
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:15 +05:00
01zulfi
f572bb1637 editor: fix insertParagraph keys
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:15 +05:00
01zulfi
773ede4b6c common: improve key formatting
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:15 +05:00
01zulfi
e4d42c8bac web: add command to show keyboard shortcuts in command palette
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
5b85067e15 web: improve contrast of keys text in keyboard shortcuts dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
0d111f3a79 web: use keys from common/keybindings in command palette dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
f6b6f2b4b7 web: support mac key formatting in keyboard shortcuts dialog
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
d55a2aa04d common: support mac keyboard formatting
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
0185eea740 editor: remove hardcoded keybind definitions
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
d44e8874b8 docs: move keyboard shortcuts script to docs/help
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
e34d33dd61 web: update key maps registeration
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:20:01 +05:00
01zulfi
4b2fb80c23 global: add keyboard shortcuts
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

global: (wip) move keyboard shortcuts metadata into common package
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

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

global: (wip) more refactoring
* minor refactor dialog
* refactor logic out from script file to keybindings file

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

common: filter keybindings based on platform && minor ui changes

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

editor: use keyboard keys from common package
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

web: remove unrelated title-box change
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>

editor: add TODO:remove comments
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-22 14:19:46 +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
3c908ac079 Merge pull request #8384 from streetwriters/themebuilder/release/1.4.0
Update theme builder to new UI
2025-07-22 09:08:02 +05:00
Abdullah Atta
563f068948 theme-builder: bump version to 1.4.0 2025-07-22 09:03:09 +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
1344b958cb Merge pull request #8382 from 01zulfi/web/note-linking-dialog
web: migrate note linking dialog lists from tanstack virtual to virtuoso
2025-07-22 08:41:38 +05:00
01zulfi
32d7cab70c web: migrate note linking dialog lists from tanstack virtual to virtuoso
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-21 14:49:41 +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
1293fa2dec Merge pull request #8365 from 01zulfi/web/fix-nav-pane-on-small-screens
web: fix nav pane on small screens
2025-07-21 12:23:50 +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
8542cbf37e Merge pull request #8381 from streetwriters/fix/email-should-not-be-required-session-expiry
Email should not be required on session expiry
2025-07-21 12:19:42 +05:00
Abdullah Atta
62c6fba9a0 web: email should not be required on session expiry 2025-07-21 12:10:43 +05:00
Abdullah Atta
2d84154226 web: show hamburger menu button on mobile 2025-07-21 12:09:58 +05:00
01zulfi
c4e306d569 web: fix nav pane on small screens
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-21 11:56:45 +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
e86f7491ef Merge pull request #8380 from streetwriters/ci/run-tests-on-pr-ref
Run tests on pr ref instead of base branch
2025-07-21 11:47:27 +05:00
Abdullah Atta
5151d83af9 ci: run tests on pr ref instead of base branch 2025-07-21 11:38:30 +05:00
Abdullah Atta
e462cb0596 Merge pull request #8379 from streetwriters/docs/remove-onenote-sn-docs
Remove standard notes and one note importer docs
2025-07-21 11:30:52 +05:00
Abdullah Atta
a30c0e4608 docs(help): remove standard notes and one note importer docs 2025-07-21 11:30:32 +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
f90e7982f7 Merge pull request #8378 from streetwriters/ci/migrate-to-pr-target
Fix tests from PRs opened from external forks
2025-07-21 11:28:11 +05:00
Abdullah Atta
40c69d7cba ci: use pull_request_target with authorization hook where required 2025-07-21 11:26:03 +05:00
Abdullah Atta
c9e6f3e532 Merge pull request #8372 from streetwriters/fix/web-tests 2025-07-21 09:54:05 +05:00
Abdullah Atta
23ee50f688 Merge pull request #8372 from streetwriters/fix/web-tests
Fix web E2E tests
2025-07-21 09:53:05 +05:00
Abdullah Atta
a7c857fb71 desktop: increase hookTimeout ton 120 seconds 2025-07-21 09:35:39 +05:00
Abdullah Atta
0f6622360c desktop: ignore error on failure to remove test artifacts 2025-07-21 09:28:26 +05:00
Abdullah Atta
578bc6e546 desktop: build app only once during tests 2025-07-21 09:19:53 +05:00
Abdullah Atta
70fd038207 desktop: add build retrying when running tests 2025-07-21 08:57:16 +05:00
Abdullah Atta
b30110a48a desktop: add support for running tests in parallel
we do this by copying the build directory instead of building it over and over
again for each test. This also makes the tests super fast.
2025-07-19 15:05:24 +05:00
Abdullah Atta
21978c4c1d desktop: patch better-sqlite3-multiple-ciphers on each test build 2025-07-19 12:32:22 +05:00
Abdullah Atta
ec48f2350d desktop: run tests sequentially
this is required since each test builds a new app and on linux & windows that
can cause issues if done in parallel
2025-07-19 11:02:32 +05:00
Abdullah Atta
fe1e3bb847 desktop: migrate tests to vitest 3 2025-07-19 11:01:57 +05:00
Abdullah Atta
69776f26d8 desktop: upgrade to vitest 3 2025-07-19 11:01:42 +05:00
Abdullah Atta
ab4092c088 desktop: fix dialog skipping in tests 2025-07-19 09:43:39 +05:00
Abdullah Atta
4948a8d7de desktop: add test artifacts cleanup on test finish 2025-07-19 09:43:23 +05:00
Abdullah Atta
0a607bce18 web: increase app lock test timeouts 2025-07-19 08:51:29 +05:00
Abdullah Atta
e59820e3d6 web: fix adding a one-time reminder before current time should not be possible 2025-07-19 08:50:22 +05:00
Abdullah Atta
85a2d25c1b web: wait for reminder dialog to close before proceeding in tests 2025-07-18 15:06:59 +05:00
Abdullah Atta
3bc7fc84f9 web: fix `if note is active in multiple tabs, moving the note to trash should
close those tabs` test
2025-07-18 14:45:06 +05:00
Abdullah Atta
249ce1ecbb web: remove new line at eof in test snapshots 2025-07-18 13:02:03 +05:00
Abdullah Atta
be7f8d9916 ci: run tests based on affected files on pr open 2025-07-18 12:59:04 +05:00
Abdullah Atta
0a0495f1a7 config: include editor/src/extensions/code-block/languages to build output 2025-07-18 12:51:11 +05:00
Abdullah Atta
2f7afa8556 web: update readonly edited note snapshots for linux & windows 2025-07-18 12:44:45 +05:00
Abdullah Atta
2f40905372 Merge pull request #8366 from Scott-will/master 2025-07-18 12:31:52 +05:00
Abdullah Atta
a6d0177153 Merge pull request #8366 from Scott-will/master
editor: maintain relative cursor location when list is sorted
2025-07-18 12:29:13 +05:00
Scott-will
b9e24888e7 editor: maintain relative cursor location when list is sorted
Signed-off-by: Scott Williams scottwill1999@gmail.com
2025-07-16 14:01:40 -07:00
Abdullah Atta
976199e5c6 Merge pull request #8364 from 01zulfi/editor/fix-readonly-links 2025-07-16 11:51:51 +05:00
Abdullah Atta
9017dae664 Merge pull request #8364 from 01zulfi/editor/fix-readonly-links
editor: fix internal links opening in new tab in readonly notes
2025-07-16 11:50:58 +05:00
01zulfi
94acc241d9 editor: fix internal links opening in new tab in readonly notes
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-16 11:47:09 +05:00
Ammar Ahmed
765f3a476d mobile: release v3.2.9 2025-07-16 11:26:19 +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
2cdb916233 Merge pull request #8363 from streetwriters/fix-app-lauch-crash-pixel-device
mobile: fix crash on app launch on pixel devices
2025-07-16 10:47:16 +05:00
Ammar Ahmed
5f14261aba mobile: fix crash on app launch on pixel devices 2025-07-16 10:11:05 +05:00
Ammar Ahmed
5ae7855c7f Merge branch 'mobile-release-3.2.8' into beta 2025-07-16 09:38:58 +05:00
Ammar Ahmed
20138505c4 Merge pull request #8361 from streetwriters/mobile-release-3.2.8
mobile: release v3.2.8
2025-07-16 09:37:35 +05:00
Ammar Ahmed
49c09bfd85 mobile: release v3.2.8 2025-07-15 11:07:33 +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
Ammar Ahmed
475edf6732 Merge pull request #8356 from streetwriters/mobile-fix-sidebar-items-update
mobile: Fix sidebar items do not update after sync
2025-07-15 10:56:47 +05:00
Ammar Ahmed
76ea89b668 Merge pull request #8351 from streetwriters/mobile-fix-color-tag-long-press-crash
mobile: fix crash on long press color tag
2025-07-15 10:56:34 +05:00
Ammar Ahmed
56b3a8df6b Merge pull request #8349 from streetwriters/mobile-fix-android-nav-bar
mobile: fix nav bar handling on android 15 and above
2025-07-15 10:56:15 +05:00
Ammar Ahmed
e02de0c0f6 Merge pull request #8336 from streetwriters/mobile-fix-crash-on-move-notebook
Fix crash on moving notebook when no notebooks selected
2025-07-15 10:55:58 +05:00
Ammar Ahmed
84bfab2a72 mobile: fix sidebar items do not update after sync 2025-07-15 09:36:00 +05:00
Ammar Ahmed
a6825a2df2 mobile: fix bottom insets 2025-07-14 15:19:15 +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
Abdullah Atta
9ba58d1a46 Merge pull request #8350 from 01zulfi/web/hide-spell-checker-languages-on-mac
web: hide spell checker language selector on mac os
2025-07-14 12:57:33 +05:00
Ammar Ahmed
29a99d3f97 mobile: fix crash on long press color tag 2025-07-14 12:48:58 +05:00
01zulfi
a8b6fe78ba web: hide spell checker language selector on mac os
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-14 12:42:49 +05:00
Ammar Ahmed
4abd69b460 mobile: fix nav bar handling on android 15 and above 2025-07-14 12:21:43 +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
5c55b49976 Merge pull request #8348 from 01zulfi/web/fix-link-in-importer
web: fix github link in importer setting
2025-07-14 11:53:39 +05:00
01zulfi
c932961dd4 web: fix github link in importer setting
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-14 11:31:30 +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
Abdullah Atta
47357c3e25 Merge pull request #8346 from 01zulfi/web/middle-click-new-note-button
web: create new tab on create note button's middle click
2025-07-14 10:52:12 +05:00
Abdullah Atta
3ec5a1cf2c Merge pull request #8345 from 01zulfi/web/fix-toolbar-bug
web: fix toolbar crash
2025-07-14 10:49:54 +05:00
01zulfi
13d8a7baf7 web: create new tab on create note button's middle click
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-14 09:21:19 +05:00
01zulfi
19b1e74d06 web: fix toolbar crash
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-14 09:12:16 +05:00
Ammar Ahmed
82de9b9b94 mobile: fix crash on moving notebook when no notebooks selected 2025-07-12 14:31:00 +05:00
Ammar Ahmed
2325f4e31c Merge branch 'mobile-release-3.2.7' into beta 2025-07-12 14:29:51 +05:00
Ammar Ahmed
867af58695 Merge pull request #8335 from streetwriters/mobile-release-3.2.7
mobile: release v3.2.7
2025-07-12 14:29:22 +05:00
Ammar Ahmed
cdd50f0258 mobile: release v3.2.7 2025-07-11 14:01:22 +05:00
Ammar Ahmed
8c8b92cc4e Merge pull request #8333 from streetwriters/mobile-fix-empty-note-bug
Fix notes open empty from notes list randomly
2025-07-11 13:57:47 +05:00
Ammar Ahmed
c0a3df0aa8 Merge branch 'mobile-fix-empty-note-bug' into beta 2025-07-11 13:57:12 +05:00
Ammar Ahmed
31dd1619f6 mobile: fix notes open empty from notes list 2025-07-11 12:54:39 +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
Ammar Ahmed
afb010e3ae Merge pull request #8331 from streetwriters/release-mobile-3.2.6
mobile: release v3.2.6
2025-07-11 11:15:19 +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
Abdullah Atta
ddebdcda43 Merge pull request #8330 from 01zulfi/monograph/fix-api-host-on-client 2025-07-11 10:51:35 +05:00
01zulfi
29c69418e9 monograph: fix API_HOST on client
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
2025-07-11 09:44:09 +05:00
Ammar Ahmed
3e565e5195 mobile: release v3.2.6 2025-07-08 14:42:46 +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
1b352d6162 Merge pull request #8320 from streetwriters/mobile-fix-crash-on-boot-android
Fix boot crash on android
2025-07-08 14:40:08 +05:00
Ammar Ahmed
258aa77cdd mobile: fix boot crash on android 2025-07-08 14:23:25 +05:00
Ammar Ahmed
bc8de87cda Merge branch 'mobile-fix-duplicate-body-notification' into beta 2025-07-08 12:25:52 +05:00
Ammar Ahmed
7789bd145c Merge pull request #8319 from streetwriters/mobile-fix-duplicate-body-notification
Fix reminder description duplicated in notification on iOS
2025-07-08 12:24:52 +05:00
Ammar Ahmed
c1457cc83f mobile: show description in notification body only 2025-07-08 12:20:40 +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
Ammar Ahmed
8b316d0abf mobile: release v3.2.5 2025-06-30 13:54:35 +05:00
Ammar Ahmed
def5059ff1 mobile: fix app stuck on logout on ios 2025-06-30 13:44:30 +05:00
Ammar Ahmed
e1febfae31 mobile: fix loading pdf screen not showing 2025-06-30 13:37:13 +05:00
Ammar Ahmed
82aa506902 mobile: fix pdf view gets stuck 2025-06-30 12:40:18 +05:00
Ammar Ahmed
de1aa40ecf mobile: fix sqlite3-fts5-html lib loading error on ios 2025-06-30 12:39:12 +05:00
Ammar Ahmed
53724b437e mobile: fix app store error invalid swift support 2025-06-28 14:24:46 +05:00
Ammar Ahmed
1592620141 mobile: release v3.2.4 2025-06-28 13:35:21 +05:00
Ammar Ahmed
28ef87a95b mobile: fix ts error 2025-06-28 13:33:24 +05:00
Ammar Ahmed
d4019eea99 mobile: fix sync taking too long to complete 2025-06-28 13:33:24 +05:00
Ammar Ahmed
d13b46cbba mobile: do not embed framework in appex on ios 2025-06-28 08:50:31 +05:00
476 changed files with 661530 additions and 106423 deletions

View File

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

View File

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

@@ -6,10 +6,16 @@ on:
branches:
- "master"
paths:
- "app/desktop/**"
- "apps/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
pull_request:
branches:
- "master"
paths:
- "apps/desktop/**"
# re-run workflow if workflow file changes
- ".github/workflows/desktop.tests.yml"
jobs:
build:
@@ -205,11 +211,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

@@ -10,6 +10,12 @@ on:
# re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml"
pull_request:
branches:
- "master"
paths:
- "packages/editor/**"
# re-run workflow if workflow file changes
- ".github/workflows/editor.tests.yml"
types:
- "ready_for_review"
- "opened"

View File

@@ -45,7 +45,7 @@ jobs:
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v3
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
platforms: linux/amd64,linux/arm64
- name: Log in to Docker Hub
uses: docker/login-action@v3
@@ -77,7 +77,7 @@ jobs:
context: apps/monograph
file: apps/monograph/Dockerfile
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
platforms: linux/amd64,linux/arm64
tags: streetwriters/monograph:${{ steps.package_metadata.outputs.app_version }},streetwriters/monograph:latest
cache-from: streetwriters/monograph:latest

View File

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

1
.gitignore vendored
View File

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

View File

@@ -24,7 +24,7 @@ Requirements:
Before you can do anything, you'll need to [install Node.js](https://nodejs.org/en/download/) v16 or later on your system.
Once you have completed the setup, the first step is to `clone` the monorepo:
1. `clone` the monorepo:
```bash
git clone https://github.com/streetwriters/notesnook.git
@@ -33,19 +33,28 @@ git clone https://github.com/streetwriters/notesnook.git
cd notesnook
```
Once you are inside the `./notesnook` directory, run the preparation step:
2. Install dependencies:
```bash
# this might take a while to complete
npm install
```
Now you can finally start the desktop app for development:
3. Run the webapp for desktop environment:
```bash
cd apps/web
npm run start:desktop
```
4. In a separate terminal session, run the desktop app from the root of the project:
```bash
npm run start:desktop
```
### Release mode
To run the app in release mode:
```bash

View File

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

View File

@@ -0,0 +1,24 @@
/*
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 { buildApp } from "./utils";
export default async function setup() {
await buildApp();
}

View File

@@ -17,22 +17,24 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test } from "vitest";
import { harness } from "./utils.js";
import assert from "assert";
import { testCleanup, test } from "./test-override.js";
test("make sure app loads", async (t) => {
await harness(t, async ({ page }) => {
await page.waitForSelector("#authForm");
test("make sure app loads", async ({
ctx: { page },
expect,
onTestFinished
}) => {
onTestFinished(testCleanup);
assert.ok(
await page.getByRole("button", { name: "Create account" }).isVisible()
);
await page.waitForSelector("#authForm");
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
expect(
await page.getByRole("button", { name: "Create account" }).isVisible()
).toBe(true);
await page.waitForSelector(".ProseMirror");
});
await page
.getByRole("button", { name: "Skip & go directly to the app" })
.click();
await page.waitForSelector(".ProseMirror");
});

View File

@@ -0,0 +1,64 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { test as vitestTest, TestContext } from "vitest";
import { buildAndLaunchApp, Fixtures, TestOptions } from "./utils";
import { mkdir, rm } from "fs/promises";
import path from "path";
import slugify from "slugify";
export const test = vitestTest.extend<Fixtures>({
options: { version: "3.0.0" } as TestOptions,
ctx: async ({ options }, use) => {
const ctx = await buildAndLaunchApp(options);
await use(ctx);
}
});
export async function testCleanup(context: TestContext) {
const ctx = (context.task.context as unknown as Fixtures).ctx;
if (context.task.result?.state === "fail") {
await mkdir("test-results", { recursive: true });
await ctx.page.screenshot({
path: path.join(
"test-results",
`${slugify(context.task.name)}-${process.platform}-${
process.arch
}-error.png`
)
});
}
await ctx.app.close();
await rm(ctx.userDataDir, {
force: true,
recursive: true,
maxRetries: 3,
retryDelay: 5000
}).catch(() => {
/*ignore */
});
await rm(ctx.outputDir, {
force: true,
recursive: true,
maxRetries: 3,
retryDelay: 5000
}).catch(() => {
/*ignore */
});
}

View File

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

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,13 +2,15 @@
"name": "@notesnook/desktop",
"productName": "Notesnook",
"description": "Your private note taking space",
"version": "3.2.3",
"version": "3.3.0",
"appAppleId": "1544027013",
"private": true,
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"sideEffects": false,
"sideEffects": [
"src/overrides.ts"
],
"exports": {
".": {
"require": {
@@ -30,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",
@@ -45,7 +47,7 @@
"@types/node": "22.15.3",
"@types/yargs": "^17.0.33",
"chokidar": "^4.0.3",
"electron": "^34.0.0",
"electron": "^36.0.0",
"electron-builder": "^26.0.12",
"esbuild": "0.21.5",
"node-abi": "^4.5.0",
@@ -55,7 +57,7 @@
"slugify": "1.6.6",
"tree-kill": "^1.2.2",
"undici": "^7.8.0",
"vitest": "2.1.8"
"vitest": "^3.2.4"
},
"optionalDependencies": {
"dmg-license": "^1.0.11"

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

@@ -17,6 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import "./overrides";
import { app, BrowserWindow, nativeTheme, shell } from "electron";
import { isDevelopment } from "./utils";
import { registerProtocol, PROTOCOL_URL } from "./utils/protocol";

View File

@@ -16,31 +16,26 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { app } from "electron";
import path from "path";
import React from "react";
import { Text } from "react-native";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import Paragraph from "../ui/typography/paragraph";
const customVersion = process.env.CUSTOM_APP_VERSION;
if (customVersion) {
app.getVersion = () => customVersion;
console.log("setting custom version:", customVersion);
}
export const Offer = ({
off = "30",
text = "on yearly plan, offer ends soon",
padding = 0
}) => {
const { colors } = useThemeColors();
return (
<Paragraph
style={{
textAlign: "center",
paddingVertical: padding
}}
size={AppFontSize.xxxl}
>
GET {off}
<Text style={{ color: colors.primary.accent }}>%</Text> OFF!{"\n"}
<Paragraph color={colors.secondary.paragraph}>{text}</Paragraph>
</Paragraph>
if (process.env.CUSTOM_USER_DATA_DIR) {
app.setPath(
"appData",
path.join(process.env.CUSTOM_USER_DATA_DIR, "AppData")
);
};
app.setPath(
"userData",
path.join(process.env.CUSTOM_USER_DATA_DIR, "UserData")
);
app.setPath(
"documents",
path.join(process.env.CUSTOM_USER_DATA_DIR, "Documents")
);
}

View File

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

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.88`;
// 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

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

View File

@@ -74,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,14 +25,22 @@ 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,99 +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");
} 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}
@@ -123,7 +53,7 @@ const Auth = ({ navigation, route }) => {
)}
<Toast context="local" />
</View>
</SafeAreaView>
);
};

View File

@@ -20,16 +20,24 @@ 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 { TouchableOpacity, View, useWindowDimensions } from "react-native";
import {
ScrollView,
TouchableOpacity,
View,
useWindowDimensions
} from "react-native";
import { SheetManager } from "react-native-actions-sheet";
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 +45,10 @@ 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 { useRoute } from "@react-navigation/native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
const LoginSteps = {
emailAuth: 1,
@@ -50,6 +59,7 @@ const LoginSteps = {
export const Login = ({ changeMode }) => {
const { colors } = useThemeColors();
const [focused, setFocused] = useState(false);
const route = useRoute();
const {
step,
setStep,
@@ -62,15 +72,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()) {
Navigation.navigate("PayWall", {
context: "signup",
state: route.params.state,
canGoBack: false
});
} else {
Progress.present();
}
});
const { width, height } = useWindowDimensions();
const isTablet = width > 600;
@@ -89,217 +108,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

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

View File

@@ -21,24 +21,33 @@ import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { db } from "../../common/database";
import { DDS } from "../../services/device-detection";
import { ToastManager } from "../../services/event-manager";
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
import 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";
import { useRoute } from "@react-navigation/native";
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,24 @@ 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();
}
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 +106,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

@@ -17,12 +17,15 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Dimensions, TextInput, View } from "react-native";
import Orientation from "react-native-orientation-locker";
import Pdf from "react-native-pdf";
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
import { MMKV } from "../../../common/database/mmkv";
import downloadAttachment from "../../../common/filesystem/download-attachment";
import { deleteCacheFileByPath, exists } from "../../../common/filesystem/io";
import { cacheDir } from "../../../common/filesystem/utils";
import { useAttachmentProgress } from "../../../hooks/use-attachment-progress";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
@@ -30,8 +33,9 @@ import {
eSubscribeEvent,
eUnSubscribeEvent
} from "../../../services/event-manager";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { sleep } from "../../../utils/time";
import { Dialog } from "../../dialog";
import BaseDialog from "../../dialog/base-dialog";
import { presentDialog } from "../../dialog/functions";
@@ -39,11 +43,6 @@ import SheetProvider from "../../sheet-provider";
import { IconButton } from "../../ui/icon-button";
import { ProgressBarComponent } from "../../ui/svg/lazy";
import Paragraph from "../../ui/typography/paragraph";
import { sleep } from "../../../utils/time";
import { MMKV } from "../../../common/database/mmkv";
import { deleteCacheFileByPath } from "../../../common/filesystem/io";
import { strings } from "@notesnook/intl";
import { DefaultAppStyles } from "../../../utils/styles";
const WIN_WIDTH = Dimensions.get("window").width;
const WIN_HEIGHT = Dimensions.get("window").height;
@@ -105,15 +104,19 @@ const PDFPreview = () => {
const open = useCallback(
async (attachment) => {
setVisible(true);
setLoading(true);
setTimeout(async () => {
setAttachment(attachment);
let hash = attachment.hash;
if (!hash) return;
if (!(await exists(hash))) setLoading(true);
const uri = await downloadAttachment(hash, false, {
silent: true,
cache: true
});
if (!(await exists(hash))) {
setVisible(false);
return;
}
const path = `${cacheDir}/${uri}`;
snapshotValue.current = snapshot.current;
setPDFSource("file://" + path);
@@ -166,8 +169,7 @@ const PDFPreview = () => {
}}
>
{loading ? (
<Animated.View
exiting={FadeOut}
<View
style={{
flex: 1,
justifyContent: "center",
@@ -188,7 +190,7 @@ const PDFPreview = () => {
>
{strings.loadingWithProgress(progress?.percent)}
</Paragraph>
</Animated.View>
</View>
) : (
<>
<View
@@ -270,49 +272,42 @@ const PDFPreview = () => {
</View>
</View>
{pdfSource ? (
<Animated.View
style={{
flex: 1
<Pdf
source={{
uri: pdfSource
}}
entering={FadeIn}
>
<Pdf
source={{
uri: pdfSource
}}
ref={pdfRef}
onLoadComplete={(numberOfPages) => {
setNumPages(numberOfPages);
}}
onPageChanged={(page) => {
setCurrentPage(page);
inputRef.current?.setNativeProps({
text: page + ""
});
saveSnapshot({
currentPage: page,
scale: snapshot?.current?.scale
});
}}
// scale={snapshotValue.current?.scale}
// onScaleChanged={(scale) => {
// saveSnapshot({
// currentPage: snapshot?.current?.currentPage,
// scale: scale
// });
// }}
page={snapshotValue?.current?.currentPage}
password={password}
maxScale={6}
onError={onError}
onPressLink={(uri) => {}}
style={{
flex: 1,
width: width,
height: Dimensions.get("window").height
}}
/>
</Animated.View>
ref={pdfRef}
onLoadComplete={(numberOfPages) => {
setNumPages(numberOfPages);
}}
onPageChanged={(page) => {
setCurrentPage(page);
inputRef.current?.setNativeProps({
text: page + ""
});
saveSnapshot({
currentPage: page,
scale: snapshot?.current?.scale
});
}}
// scale={snapshotValue.current?.scale}
// onScaleChanged={(scale) => {
// saveSnapshot({
// currentPage: snapshot?.current?.currentPage,
// scale: scale
// });
// }}
page={snapshotValue?.current?.currentPage}
password={password}
maxScale={6}
onError={onError}
onPressLink={(uri) => {}}
style={{
flex: 1,
width: width,
height: Dimensions.get("window").height
}}
/>
) : null}
</>
)}

View File

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

View File

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

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

@@ -153,7 +153,6 @@ export const SearchResult = (props: SearchResultProps) => {
for (let i = 0; i <= index; i++) {
activeIndex += props.item.content[i].length;
}
console.log(activeIndex);
openNote(activeIndex);
}}
>

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();
@@ -73,9 +75,6 @@ const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
testID={notesnook.ids.dialogs.actionsheet.color(item.colorCode)}
key={item.id}
onPress={toggleColor}
onLongPress={(event) => {
NativeTooltip.show(event, item.title, NativeTooltip.POSITIONS.TOP);
}}
style={{
width: 35,
height: 35,
@@ -98,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";
@@ -112,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
@@ -136,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
}}
@@ -175,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

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

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";
@@ -65,7 +66,9 @@ const COLUMN_BAR_ITEMS: ActionId[] = [
"edit-notebook",
"move-notes",
"move-notebook",
"edit-reminder",
"pin",
"disable-reminder",
"default-notebook",
"default-tag",
"default-homepage",
@@ -143,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
@@ -218,7 +222,8 @@ export const Items = ({
borderRadius: 0,
justifyContent: "flex-start",
alignSelf: "flex-start",
width: "100%"
width: "100%",
opacity: item.locked ? 0.5 : 1
}}
/>
),
@@ -242,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
@@ -250,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
@@ -265,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
@@ -322,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,18 +17,18 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import { db } from "../../common/database";
import ManageTags from "../../screens/manage-tags";
import { TaggedNotes } from "../../screens/notes/tagged";
import { useThemeColors } from "@notesnook/theme";
import { AppFontSize } from "../../utils/size";
import { DefaultAppStyles } from "../../utils/styles";
import { sleep } from "../../utils/time";
import { 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";
export const Tags = ({ item, close }) => {
const { colors } = useThemeColors();
@@ -86,7 +86,6 @@ export const TagStrip = ({ item, close }) => {
flexDirection: "row",
flexWrap: "wrap",
alignItems: "center",
marginTop: DefaultAppStyles.GAP_VERTICAL,
gap: 5
}}
>
@@ -107,16 +106,13 @@ const TagItem = ({ tag, close }) => {
const style = {
paddingHorizontal: 0,
borderRadius: 100,
marginTop: 0,
backgroundColor: "transparent"
paddingVertical: DefaultAppStyles.GAP_VERTICAL_SMALL
};
return (
<Button
onPress={onPress}
title={"#" + tag.title}
type="plain"
height={20}
fontSize={AppFontSize.xs}
style={style}
textStyle={{

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();
@@ -197,16 +197,6 @@ export const SelectionHeader = React.memo(
}
]
: [
{
title: strings.move(),
onPress: async () => {
const ids = selectedItemsList;
const notebooks = await db.notebooks.all.items(ids);
MoveNotebook.present(notebooks);
},
visible: renderedInRoute === "Notebooks",
icon: "arrow-right-bold-box-outline"
},
{
title: strings.manageTags(),
onPress: async () => {

View File

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

View File

@@ -0,0 +1,490 @@
/*
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 { 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
}}
>
<WebView
source={{
uri: checkoutUrl
}}
onMessage={(message) => {
try {
const data = JSON.parse(message.nativeEvent.data);
if (data.success) {
pricingPlans.finish();
}
} catch (e) {}
}}
domStorageEnabled
javaScriptEnabled
cacheEnabled
enabled
style={{
flex: 1
}}
/>
</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) {
setCheckoutUrl(
await db.subscriptions.checkoutUrl(
(pricingPlans.selectedProduct as Plan).plan,
(pricingPlans.selectedProduct as Plan).period
)
);
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
}}
@@ -310,6 +300,7 @@ const ExportNotesSheet = ({
ToastManager.error(e as Error);
});
} else {
await sleep(500);
FileViewer.open(result?.filePath, {
showOpenWithDialog: true,
showAppsSuggestions: true
@@ -327,7 +318,6 @@ const ExportNotesSheet = ({
title={strings.share()}
type="secondaryAccented"
width={250}
fontSize={AppFontSize.md}
style={{
marginTop: DefaultAppStyles.GAP_VERTICAL
}}
@@ -338,6 +328,7 @@ const ExportNotesSheet = ({
.getState()
.setAppDidEnterBackgroundForAction(true);
if (Platform.OS === "ios") {
await sleep(500);
Share.open({
url: result?.fileDir + result.fileName
}).catch(() => {
@@ -358,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

@@ -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,
@@ -38,13 +39,14 @@ import { eCloseSheet, eOnNotebookUpdated } from "../../../utils/events";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { sleep } from "../../../utils/time";
import { Dialog } from "../../dialog";
import { Properties } from "../../properties";
import SheetProvider from "../../sheet-provider";
import { NotebookItem } from "../../side-menu/notebook-item";
import { useSideMenuNotebookTreeStore } from "../../side-menu/stores";
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,
@@ -60,13 +62,8 @@ export const Notebooks = (props: {
close?: (ctx?: string) => void;
}) => {
const tree = useNotebookTreeStore((state) => state.tree);
const [isLoading, setIsLoading] = useState(true);
const { colors } = useThemeColors();
const [notebooks, setNotebooks] = useState<Notebook[]>();
const [filteredNotebooks, setFilteredNotebooks] =
React.useState<VirtualizedGrouping<Notebook>>();
const searchTimer = React.useRef<NodeJS.Timeout>();
const lastQuery = React.useRef<string>();
const loadRootNotebooks = React.useCallback(async () => {
const notebooks = await db.relations
.from(
@@ -95,7 +92,6 @@ export const Notebooks = (props: {
useEffect(() => {
(async () => {
loadRootNotebooks();
setIsLoading(false);
})();
}, [loadRootNotebooks]);
@@ -151,7 +147,7 @@ export const Notebooks = (props: {
height: 400
}}
>
<SheetProvider context="local" />
<Dialog context="local" />
<View
style={{
@@ -174,8 +170,22 @@ export const Notebooks = (props: {
height: 30
}}
name="plus"
onPress={() => {
AddNotebookSheet.present(props.rootNotebook, undefined, "local");
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>
@@ -244,20 +254,18 @@ const NotebookItemWrapper = React.memo(
const onItemUpdate = React.useCallback(async () => {
const notebook = await db.notebooks.notebook(item.notebook.id);
if (notebook) {
useSideMenuNotebookTreeStore
.getState()
.updateItem(item.notebook.id, notebook);
useNotebookTreeStore.getState().updateItem(item.notebook.id, notebook);
if (expanded) {
useSideMenuNotebookTreeStore
useNotebookTreeStore
.getState()
.setTree(
await useSideMenuNotebookTreeStore
await useNotebookTreeStore
.getState()
.fetchAndAdd(item.notebook.id, item.depth + 1)
);
}
} else {
useSideMenuNotebookTreeStore.getState().removeItem(item.notebook.id);
useNotebookTreeStore.getState().removeItem(item.notebook.id);
}
}, [expanded, item.depth, item.notebook.id]);
@@ -274,17 +282,15 @@ const NotebookItemWrapper = React.memo(
onToggleExpanded={async () => {
useNotebookExpandedStore.getState().setExpanded(item.notebook.id);
if (!expanded) {
useSideMenuNotebookTreeStore
useNotebookTreeStore
.getState()
.setTree(
await useSideMenuNotebookTreeStore
await useNotebookTreeStore
.getState()
.fetchAndAdd(item.notebook.id, item.depth + 1)
);
} else {
useSideMenuNotebookTreeStore
.getState()
.removeChildren(item.notebook.id);
useNotebookTreeStore.getState().removeChildren(item.notebook.id);
}
}}
selected={selected}

View File

@@ -0,0 +1,262 @@
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 { 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(() => {
console.log("PaywallSheet mounted with feature:", props.feature);
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 (
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>) => {
presentSheet({
component: <PaywallSheet feature={feature} />
});
};

View File

@@ -0,0 +1,130 @@
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";
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() ? 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

@@ -40,6 +40,7 @@ import Sync from "../../../services/sync";
import Clipboard from "@react-native-clipboard/clipboard";
import { logoutUser } from "../../../screens/settings/logout";
import { sleep } from "../../../utils/time";
export const UserSheet = () => {
const ref = useSheetRef();
const { colors } = useThemeColors();
@@ -310,10 +311,10 @@ export const UserSheet = () => {
{
icon: "logout",
title: strings.logout(),
onPress: () => {
console.log("logout");
logoutUser();
onPress: async () => {
ref.current?.hide();
await sleep(300);
logoutUser();
},
hidden: !user
}

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,
@@ -211,6 +213,14 @@ const TabBar = (
const ids = useSideMenuNotebookSelectionStore
.getState()
.getSelectedItemIds();
if (!ids.length) {
ToastManager.show({
context: "local",
type: "error",
message: strings.noNotebooksSelectedToMove()
});
return;
}
const notebooks = await db.notebooks.all.items(ids);
Navigation.navigate("MoveNotebook", {
selectedNotebooks: notebooks
@@ -339,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

@@ -49,7 +49,8 @@ export const NotebookItem = ({
onPress,
onLongPress,
onAddNotebook,
canDisableSelectionMode
canDisableSelectionMode,
disableExpand
}: {
index: number;
item: TreeItem;
@@ -64,6 +65,7 @@ export const NotebookItem = ({
onLongPress?: () => void;
onAddNotebook?: () => void;
canDisableSelectionMode?: boolean;
disableExpand?: boolean;
}) => {
const notebook = item.notebook;
const isFocused = focused;
@@ -166,7 +168,7 @@ export const NotebookItem = ({
}
testID={item.hasChildren ? `expand-notebook-${index}` : ""}
onPress={() => {
if (item.hasChildren) {
if (item.hasChildren && !disableExpand) {
onToggleExpanded?.();
} else {
onPress?.();
@@ -182,7 +184,7 @@ export const NotebookItem = ({
borderRadius: defaultBorderRadius
}}
name={
!item.hasChildren
!item.hasChildren || disableExpand
? "book-outline"
: expanded
? "chevron-down"

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 { useMenuStore } from "../../stores/use-menu-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { MenuItemsList } from "../../utils/menu-items";
import { useUserStore } from "../../stores/use-user-store";
import { SUBSCRIPTION_STATUS } from "../../utils/constants";
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";
import { MenuItemsList } from "../../utils/menu-items";
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,15 @@ export function SideMenuHome() {
paddingVertical: DefaultAppStyles.GAP_VERTICAL
}}
>
{subscriptionType === SUBSCRIPTION_STATUS.TRIAL ||
subscriptionType === SUBSCRIPTION_STATUS.BASIC ? (
{subscriptionType === SubscriptionPlan.FREE ||
!subscriptionType ||
!user ? (
<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

@@ -20,8 +20,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Notebook } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import { FlashList } from "@shopify/flash-list";
import React, { useEffect, useState } from "react";
import { FlatList, TextInput, View } from "react-native";
import { TextInput, View } from "react-native";
import { db } from "../../common/database";
import NotebookScreen from "../../screens/notebook";
import Navigation from "../../services/navigation";
@@ -42,6 +43,7 @@ import {
useSideMenuNotebookSelectionStore.setState({
multiSelect: true
});
export const SideMenuNotebooks = () => {
const tree = useSideMenuNotebookTreeStore((state) => state.tree);
const [notebooks, loading] = useNotebooks();
@@ -64,13 +66,19 @@ export const SideMenuNotebooks = () => {
const updateNotebooks = React.useCallback(() => {
if (lastQuery.current) {
// useSideMenuNotebookTreeStore.setState({
// isSearching: true
// });
db.lookup
.notebooks(lastQuery.current)
.sorted()
.sorted(db.settings.getGroupOptions("notebooks"))
.then((filtered) => {
setFilteredNotebooks(filtered);
});
} else {
// useSideMenuNotebookTreeStore.setState({
// isSearching: false
// });
setFilteredNotebooks(notebooks);
}
}, [notebooks]);
@@ -137,13 +145,13 @@ export const SideMenuNotebooks = () => {
/>
) : (
<>
<FlatList
<FlashList
data={tree}
bounces={false}
bouncesZoom={false}
overScrollMode="never"
keyExtractor={(item) => item.notebook.id}
windowSize={3}
// keyExtractor={(item,) => item.notebook.id}
estimatedItemSize={30}
ListHeaderComponent={
<View
style={{
@@ -196,7 +204,9 @@ const NotebookItemWrapper = React.memo(
const expanded = useSideMenuNotebookExpandedStore(
(state) => state.expanded[item.notebook.id]
);
const disableExpand = useSideMenuNotebookTreeStore(
(state) => state.isSearching
);
const selectionEnabled = useSideMenuNotebookSelectionStore(
(state) => state.enabled
);
@@ -256,6 +266,7 @@ const NotebookItemWrapper = React.memo(
.removeChildren(item.notebook.id);
}
}}
disableExpand={disableExpand}
selected={selected}
selectionEnabled={selectionEnabled}
selectionStore={useSideMenuNotebookSelectionStore}

View File

@@ -220,7 +220,7 @@ export const SideMenuTags = () => {
if (lastQuery.current) {
db.lookup
.tags(lastQuery.current.trim())
.sorted()
.sorted(db.settings.getGroupOptions("tags"))
.then(async (filtered) => {
setFilteredTags(filtered);
});

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

@@ -154,7 +154,7 @@ const Input = ({
isError = customValidator && value === customValidator();
break;
case "url":
isError = isURL(value);
isError = isURL(value, { allow_underscores: true });
break;
case "phonenumber": {
const result = phone(value, {
@@ -237,8 +237,8 @@ const Input = ({
onPress && loading ? colors.primary.accent : colors.primary.paragraph,
flexGrow: 1,
flexShrink: 1,
paddingBottom: DefaultAppStyles.GAP_VERTICAL - 2,
paddingTop: DefaultAppStyles.GAP_VERTICAL - 2,
paddingBottom: DefaultAppStyles.GAP_VERTICAL - 4,
paddingTop: DefaultAppStyles.GAP_VERTICAL - 4,
fontFamily: "OpenSans-Regular",
...(inputStyle as ViewStyle)
};

View File

@@ -54,6 +54,7 @@ type ButtonTypes =
| "accent"
| "shade"
| "secondary"
| "selectedAccent"
| "secondaryAccented"
| "inverted"
| "white"
@@ -126,6 +127,18 @@ const buttonTypes = (
isDark
)
},
selectedAccent: {
primary: colors.selected.accent,
text: colors.selected.accentForeground,
selected: colors.selected.accent,
borderWidth: 0.8,
borderColor: getColorLinearShade(colors.selected.accent, 0.05, isDark),
borderSelectedColor: getColorLinearShade(
colors.selected.accent,
0.05,
isDark
)
},
secondaryAccented: {
primary: colors.secondary.background,
text: colors.primary.accent,

View File

@@ -23,12 +23,10 @@ import { Platform, View } from "react-native";
import ActionSheet from "react-native-actions-sheet";
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
import { useSettingStore } from "../../../stores/use-setting-store";
import { PremiumToast } from "../../premium/premium-toast";
import { Toast } from "../../toast";
import { useAppState } from "../../../hooks/use-app-state";
import SettingsService from "../../../services/settings";
import { useUserStore } from "../../../stores/use-user-store";
import { getContainerBorder } from "../../../utils/colors";
import { NotesnookModule } from "../../../utils/notesnook-module";
import { Toast } from "../../toast";
/**
*
@@ -63,28 +61,35 @@ const SheetWrapper = ({
const locked = useUserStore((state) => state.appLocked);
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",
borderBottomRightRadius: 0,
borderBottomLeftRadius: 0,
...getContainerBorder(colors.primary.border, 0.5),
borderBottomWidth: 0
borderBottomWidth: 0,
paddingBottom:
Platform.OS === "android" && !insets.bottom
? isGestureNavigationEnabled
? 0
: 30
: 0
};
}, [
colors.primary.background,
colors.primary.border,
largeTablet,
smallTablet,
width
width,
insets.bottom
]);
const _onOpen = () => {
@@ -127,6 +132,7 @@ const SheetWrapper = ({
width: 100,
backgroundColor: colors.secondary.background
}}
safeAreaInsets={insets}
statusBarTranslucent
drawUnderStatusBar={true}
containerStyle={style}
@@ -151,11 +157,6 @@ const SheetWrapper = ({
ExtraOverlayComponent={
<>
{overlay}
<PremiumToast
context="sheet"
close={() => fwdRef?.current?.hide()}
offset={50}
/>
<Toast context="local" />
</>
}
@@ -165,10 +166,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,287 +60,12 @@ 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: [
{
title: strings.emailConfirmed(),
text: strings.emailNotConfirmedDesc(),
text: strings.emailConfirmedDesc(),
walkthroughItem: (colors) => (
<SvgView src={WELCOME_SVG(colors.primary.paragraph)} />
),
@@ -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

@@ -19,9 +19,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { FeatureType } from "./components/sheets/new-feature";
export const features: FeatureType[] = [
{
title: "Improved Search",
body: "Improved search results with better relevance and highlighting."
}
];
export const features: FeatureType[] = [];

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,14 +566,32 @@ 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",
title: strings.editNotebook(),
icon: "square-edit-outline",
onPress: async () => {
close();
await sleep(300);
AddNotebookSheet.present(item);
}
},
@@ -539,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
};
@@ -547,7 +629,9 @@ export const useActions = ({
}
close();
},
checked: defaultNotebook === item.id
checked: defaultNotebook === item.id,
locked: !features?.defaultNotebookAndTag.isAllowed
},
{
id: "move-notes",
@@ -598,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
@@ -680,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);
@@ -909,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();
},
@@ -923,7 +1054,8 @@ export const useActions = ({
iconSize: 20
}
});
}
},
locked: !features?.activeReminders.isAllowed
},
{
@@ -1035,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,104 @@
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);
Navigation.navigate("PayWall", {
context: "logged-in"
});
}
});
}, 1000);
}
});
}
return () => {
unsub?.();
};
}, [features, plan]);
return true;
}

View File

@@ -26,12 +26,14 @@ import { useCallback } from "react";
* @returns Is keyboard floating or not
*/
const useIsFloatingKeyboard = () => {
const { width } = useWindowDimensions();
const { width, height } = useWindowDimensions();
const [floating, setFloating] = useState<boolean>(false);
const onKeyboardWillChangeFrame = useCallback(
(event: KeyboardEvent) => {
setFloating(event.endCoordinates.width < width);
setFloating(
event.endCoordinates.width === 0 || event.endCoordinates.width < width
);
},
[width]
);
@@ -41,8 +43,13 @@ const useIsFloatingKeyboard = () => {
"keyboardWillChangeFrame",
onKeyboardWillChangeFrame
);
const sub2 = Keyboard.addListener(
"keyboardWillShow",
onKeyboardWillChangeFrame
);
return () => {
sub1?.remove();
sub2?.remove();
};
}, [onKeyboardWillChangeFrame, width]);

View File

@@ -0,0 +1,712 @@
/*
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";
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") {
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)
return `${(product as Plan).price.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 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)
return `${(product as Plan).price.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

@@ -30,6 +30,10 @@ import React, {
useState
} from "react";
import { Dimensions, LayoutChangeEvent, Platform, View } from "react-native";
import Orientation, {
OrientationType,
useDeviceOrientationChange
} from "react-native-orientation-locker";
import Animated, {
useAnimatedStyle,
useSharedValue,
@@ -67,10 +71,6 @@ import {
} from "../utils/events";
import { editorRef, fluidTabsRef } from "../utils/global-refs";
import { AppNavigationStack } from "./navigation-stack";
import Orientation, {
OrientationType,
useDeviceOrientationChange
} from "react-native-orientation-locker";
const MOBILE_SIDEBAR_SIZE = 0.85;
@@ -421,7 +421,6 @@ export const FluidPanelsView = React.memo(
height: "100%",
width: "100%",
backgroundColor: colors.primary.background,
paddingBottom: Platform.OS === "android" ? insets?.bottom : 0,
marginRight:
orientation === "LANDSCAPE-RIGHT" && Platform.OS === "ios"
? insets.right
@@ -530,17 +529,20 @@ const onChangeTab = async (event: { i: number; from: number }) => {
activateKeepAwake();
eSendEvent(eOnEnterEditor);
if (!useTabStore.getState().getCurrentNoteId()) {
eSendEvent(eOnLoadNote, {
newNote: true
});
} else {
if (
useTabStore.getState().getTab(useTabStore.getState().currentTab)
?.session?.locked
) {
eSendEvent(eUnlockNote);
}
if (
useTabStore.getState().getTab(useTabStore.getState().currentTab)?.session
?.locked
) {
eSendEvent(eUnlockNote);
}
if (
fluidTabsRef.current?.tabChangedFromSwipeAction.value &&
!useTabStore.getState().getNoteIdForTab(useTabStore.getState().currentTab)
) {
editorController?.current?.commands?.focus(
useTabStore.getState().currentTab
);
}
} else {
if (event.from === 2) {

View File

@@ -30,6 +30,8 @@ import useNavigationStore, {
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();
@@ -46,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;
@@ -57,7 +60,7 @@ const AppNavigation = React.memo(
React.useEffect(() => {
(async () => {
if (loading) return;
if (!homepageV2) {
if (!homepageV2 || !customHomepageFeature?.isAllowed) {
setHome({
name: "Notes",
params: undefined
@@ -249,18 +252,24 @@ 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 onStateChange = React.useCallback(() => {
if (useSelectionStore.getState().selectionMode) {
clearSelection();
}
hideAllTooltips();
}, [clearSelection]);
const onStateChange = React.useCallback(
(state: any) => {
if (useSelectionStore.getState().selectionMode) {
clearSelection();
}
setTimeout(() => {
Navigation.resetRootState(state);
}, 1000);
hideAllTooltips();
},
[clearSelection]
);
return (
<NavigationContainer onStateChange={onStateChange} ref={rootNavigatorRef}>
@@ -346,6 +355,13 @@ export const RootNavigation = () => {
return AddReminder;
}}
/>
<RootStack.Screen
name="PayWall"
getComponent={() => {
PayWall = PayWall || require("../components/paywall").default;
return PayWall;
}}
/>
</RootStack.Navigator>
</NavigationContainer>
);

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