mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 10:09:26 +02:00
Compare commits
517 Commits
3.2.3-andr
...
mobile-ref
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aef8194f9 | ||
|
|
c77050eccc | ||
|
|
f40ccdae41 | ||
|
|
0a0aa09eea | ||
|
|
e2c55fb758 | ||
|
|
2cb087972c | ||
|
|
41a32ede15 | ||
|
|
e2159b7614 | ||
|
|
ee33875edf | ||
|
|
28681fb1b0 | ||
|
|
33194dc9f0 | ||
|
|
7509766df2 | ||
|
|
30a02a4977 | ||
|
|
f67f4138c3 | ||
|
|
49d51da1bd | ||
|
|
17bfe4e075 | ||
|
|
d5905a4e5f | ||
|
|
e1b46d33f4 | ||
|
|
ce7c003c54 | ||
|
|
7f473ce7c8 | ||
|
|
c239f490b0 | ||
|
|
1ad1915fcc | ||
|
|
14029de484 | ||
|
|
5d07db366b | ||
|
|
9e265dbecf | ||
|
|
717bce36e6 | ||
|
|
17831cdd8c | ||
|
|
02e37c6ed4 | ||
|
|
16b52e9633 | ||
|
|
77eee945ca | ||
|
|
f21f235d37 | ||
|
|
2949fcde38 | ||
|
|
6ae1674de5 | ||
|
|
2bd74e043a | ||
|
|
1a05acbe9a | ||
|
|
a0597e13fe | ||
|
|
d11d665395 | ||
|
|
ab2758ac79 | ||
|
|
bb682a6f6e | ||
|
|
4377b8b340 | ||
|
|
eb0c9b823b | ||
|
|
a595b12431 | ||
|
|
ebd9b45cac | ||
|
|
5ae024f20a | ||
|
|
9ae93dec4c | ||
|
|
93ecc9b1ac | ||
|
|
28455f8b31 | ||
|
|
b49be89b06 | ||
|
|
23e0748137 | ||
|
|
2f6d222cca | ||
|
|
ba1cfe05c4 | ||
|
|
6f80334296 | ||
|
|
86da8a3620 | ||
|
|
11702c9209 | ||
|
|
240d9b533f | ||
|
|
8dc12f01b9 | ||
|
|
3b0dce6c6f | ||
|
|
901dc4fe6d | ||
|
|
61748f40b6 | ||
|
|
871824f857 | ||
|
|
032c321568 | ||
|
|
4883f02b62 | ||
|
|
b43a8cbafe | ||
|
|
11c1fe3853 | ||
|
|
4a4576ebd5 | ||
|
|
0b5128b13b | ||
|
|
d1dbb85459 | ||
|
|
ae1005d5f9 | ||
|
|
11e6074e21 | ||
|
|
ea9aa2a136 | ||
|
|
203fe5bee0 | ||
|
|
4262641567 | ||
|
|
c0f79be78c | ||
|
|
239e301042 | ||
|
|
23bb80bbd6 | ||
|
|
d7232a4f1f | ||
|
|
2bfc1af8ff | ||
|
|
3ac6873be4 | ||
|
|
ce487a108c | ||
|
|
827930e28f | ||
|
|
8a13cf01b4 | ||
|
|
641c541305 | ||
|
|
9285bae0cc | ||
|
|
954ac3154f | ||
|
|
571f3242e2 | ||
|
|
9052948b8a | ||
|
|
af91a6ee98 | ||
|
|
16633dbd05 | ||
|
|
8a506edd37 | ||
|
|
fdbac8e608 | ||
|
|
cdca90bd54 | ||
|
|
fb7d8b0893 | ||
|
|
a49bc92a13 | ||
|
|
58c1f5e3ca | ||
|
|
f1a0c1b447 | ||
|
|
d8534da4df | ||
|
|
28daa38d80 | ||
|
|
5c275adda8 | ||
|
|
26ecf2a7fd | ||
|
|
e23c0f55b7 | ||
|
|
a6b03635c7 | ||
|
|
6587541729 | ||
|
|
bc4bfa63f7 | ||
|
|
bdda2d4987 | ||
|
|
ab34073870 | ||
|
|
152a59fe26 | ||
|
|
cf169eb6d6 | ||
|
|
16da396289 | ||
|
|
7ad3fd4057 | ||
|
|
aecf4f1ee8 | ||
|
|
8f48fcd788 | ||
|
|
cf64fb3743 | ||
|
|
a21111368e | ||
|
|
383b914c52 | ||
|
|
555255a923 | ||
|
|
cd5a2ab179 | ||
|
|
48903f924c | ||
|
|
7d46dd4fb0 | ||
|
|
cfa9f11f44 | ||
|
|
1377ac38db | ||
|
|
d88b30b538 | ||
|
|
cfe0cded15 | ||
|
|
322f6b7aae | ||
|
|
62ae26c464 | ||
|
|
c920c999eb | ||
|
|
288f601651 | ||
|
|
d4f902f63d | ||
|
|
7dddebef2e | ||
|
|
dc1498d672 | ||
|
|
eb36c1d7cb | ||
|
|
29c119a945 | ||
|
|
860bc10747 | ||
|
|
215ca23190 | ||
|
|
4b9d9e4353 | ||
|
|
fd5d4e56ea | ||
|
|
9801d117a1 | ||
|
|
3c4fb8cb53 | ||
|
|
ae17b34be9 | ||
|
|
1a544754ef | ||
|
|
1ddfd15ee9 | ||
|
|
035126ab2f | ||
|
|
e157ae8bc8 | ||
|
|
25b0503a25 | ||
|
|
ef235dd0aa | ||
|
|
699080048d | ||
|
|
3c9b315f78 | ||
|
|
1464006531 | ||
|
|
6921fcb3ae | ||
|
|
286f8dd7cc | ||
|
|
d3e3ef5986 | ||
|
|
ea1ffc59b8 | ||
|
|
5524886868 | ||
|
|
68b63cf635 | ||
|
|
74de09e441 | ||
|
|
b82e9598bd | ||
|
|
25889192c4 | ||
|
|
76eac69f94 | ||
|
|
97f8f1e3c3 | ||
|
|
8722b6bc68 | ||
|
|
8e8d2a4083 | ||
|
|
4caac77706 | ||
|
|
0f73f266b2 | ||
|
|
8c70abda04 | ||
|
|
6881a5f432 | ||
|
|
22e9ae6bf8 | ||
|
|
eb73de6d8d | ||
|
|
1af7f1bd7f | ||
|
|
3a0273a12b | ||
|
|
05795ca052 | ||
|
|
a5746c56eb | ||
|
|
80f66ab0ba | ||
|
|
44e62f0eb0 | ||
|
|
a6e107f61d | ||
|
|
31bb78bfca | ||
|
|
8bb9308173 | ||
|
|
46d279ca3b | ||
|
|
0a5bdaaa7f | ||
|
|
3480b8c5da | ||
|
|
32e1fb5f56 | ||
|
|
d94e8d04c8 | ||
|
|
26e2b380b3 | ||
|
|
06710ff416 | ||
|
|
32a7f866c3 | ||
|
|
86b650712e | ||
|
|
0178e455f9 | ||
|
|
39693966c1 | ||
|
|
dc08a39173 | ||
|
|
1be518510a | ||
|
|
e513081010 | ||
|
|
9bc8bddc12 | ||
|
|
986b99f62e | ||
|
|
16092a4185 | ||
|
|
26eafcfee9 | ||
|
|
f20c1ef4f4 | ||
|
|
3f076d90c8 | ||
|
|
df70fee70e | ||
|
|
4df9c7d9f7 | ||
|
|
59916b924c | ||
|
|
aafce93117 | ||
|
|
97ef91abdb | ||
|
|
b426d61c3a | ||
|
|
f08c484d40 | ||
|
|
b03a4559c5 | ||
|
|
d2f491ed66 | ||
|
|
1a2a1da086 | ||
|
|
872f4d7e91 | ||
|
|
bce063e1bd | ||
|
|
e6dc5ddfed | ||
|
|
bcbcd516f6 | ||
|
|
24da82313e | ||
|
|
5fe29af51a | ||
|
|
fbe551eb29 | ||
|
|
c097c4d457 | ||
|
|
a792a3d6e4 | ||
|
|
28f1f276ce | ||
|
|
9470d2b61f | ||
|
|
95795a20c2 | ||
|
|
64b7b57b3e | ||
|
|
5e05d96487 | ||
|
|
18c9912068 | ||
|
|
cd85753252 | ||
|
|
bdb135036c | ||
|
|
5d70f58a0d | ||
|
|
b7d2d66e82 | ||
|
|
6b028ef850 | ||
|
|
8f85795e53 | ||
|
|
a0b7dff5fd | ||
|
|
352018c042 | ||
|
|
2b1ac822ed | ||
|
|
b3c3890443 | ||
|
|
99e642780e | ||
|
|
47088a0a66 | ||
|
|
fd48684396 | ||
|
|
9b25ab8708 | ||
|
|
c725ba8530 | ||
|
|
98287fbfc4 | ||
|
|
3260695a09 | ||
|
|
3a3434b021 | ||
|
|
9f24bd2250 | ||
|
|
94f2a216ad | ||
|
|
aeb4c984eb | ||
|
|
78e3ee5fd2 | ||
|
|
e94a2b2aa1 | ||
|
|
1ddcf44eb0 | ||
|
|
5c5fb2ba2c | ||
|
|
9344eadf34 | ||
|
|
aa4995d871 | ||
|
|
e40c42afc3 | ||
|
|
f818c41a02 | ||
|
|
178d7e7471 | ||
|
|
3f9d954e2f | ||
|
|
48a3eee232 | ||
|
|
f1fcd227e7 | ||
|
|
84af1a9411 | ||
|
|
44d910121c | ||
|
|
1258a95aad | ||
|
|
295ab2c073 | ||
|
|
7aa350a9d2 | ||
|
|
ee76e43fca | ||
|
|
1379193065 | ||
|
|
0fa3598783 | ||
|
|
02960fb5a9 | ||
|
|
a7fa42e944 | ||
|
|
02ab8e3938 | ||
|
|
01c629f612 | ||
|
|
5c120676e1 | ||
|
|
3821dc29ef | ||
|
|
3c51f73427 | ||
|
|
8db373ee34 | ||
|
|
1be6e6b444 | ||
|
|
adcb73a2fc | ||
|
|
4c97b3481d | ||
|
|
38579c37e1 | ||
|
|
4785544e18 | ||
|
|
7812c15b48 | ||
|
|
251145ea74 | ||
|
|
5e4972c1d0 | ||
|
|
64bfc80a42 | ||
|
|
b841c6e0eb | ||
|
|
7241dce7a6 | ||
|
|
8f25f9ba3e | ||
|
|
6ab58c68e1 | ||
|
|
ae5b422965 | ||
|
|
55b6be0f15 | ||
|
|
f9324d9da5 | ||
|
|
2362902675 | ||
|
|
dfe6a73ac1 | ||
|
|
afcb8835d2 | ||
|
|
9dc9f186e1 | ||
|
|
875125cfe4 | ||
|
|
32d9cdd265 | ||
|
|
669f830dd7 | ||
|
|
09deae9dbf | ||
|
|
337582ac70 | ||
|
|
820c78ce81 | ||
|
|
23924a34a3 | ||
|
|
428f1e746a | ||
|
|
565e692af8 | ||
|
|
94e4721663 | ||
|
|
8157c0812a | ||
|
|
bc36837cd0 | ||
|
|
33540b000c | ||
|
|
5a63a45d00 | ||
|
|
6eb1f2a4a7 | ||
|
|
8fce6b5eb6 | ||
|
|
5bd1999269 | ||
|
|
4e44661e1b | ||
|
|
827ca2e427 | ||
|
|
81fd1c097e | ||
|
|
aaf82f359b | ||
|
|
aa7f14ae79 | ||
|
|
0ad63da4a2 | ||
|
|
2885b9ac3e | ||
|
|
1ae9e04e7a | ||
|
|
693311a3db | ||
|
|
a15ab99056 | ||
|
|
222cdbfa63 | ||
|
|
a1d8b34fb4 | ||
|
|
35d08a6dfa | ||
|
|
874352e753 | ||
|
|
6bb5e44442 | ||
|
|
49d1ab4be5 | ||
|
|
54a0d0fd0c | ||
|
|
65a80dbd46 | ||
|
|
dfeb01320d | ||
|
|
e55456f492 | ||
|
|
d33209afcc | ||
|
|
3f9fce0d34 | ||
|
|
2ae094cf1f | ||
|
|
5530270b06 | ||
|
|
bc224f1674 | ||
|
|
8bb1582cca | ||
|
|
c759408fdc | ||
|
|
d02125a93c | ||
|
|
30553ed0f1 | ||
|
|
d91ce4e936 | ||
|
|
b315e70e1e | ||
|
|
deb902549b | ||
|
|
d6c6db68ba | ||
|
|
e41ed07805 | ||
|
|
f6e8dc0a88 | ||
|
|
0738b96161 | ||
|
|
044dd5ce59 | ||
|
|
6cca06fcbc | ||
|
|
50feebc0b0 | ||
|
|
b24830666c | ||
|
|
470802c3e2 | ||
|
|
0e869708f7 | ||
|
|
7e0e1eac93 | ||
|
|
58cbf653b2 | ||
|
|
4b35f2ecd2 | ||
|
|
48d9a863ec | ||
|
|
1d616cf68f | ||
|
|
efda6f86ae | ||
|
|
cdf6a4bb5b | ||
|
|
1b3735e2b2 | ||
|
|
54008a0de2 | ||
|
|
850fc6b066 | ||
|
|
67a398f583 | ||
|
|
8a62070c8e | ||
|
|
8aa31b8380 | ||
|
|
f445a963f9 | ||
|
|
d6e3f27612 | ||
|
|
4688d74edb | ||
|
|
e88c755ba8 | ||
|
|
1985007039 | ||
|
|
039c8faabe | ||
|
|
fb1eb234b6 | ||
|
|
7e70cd96b9 | ||
|
|
f9a89783ed | ||
|
|
26282352cb | ||
|
|
7f3c74b55b | ||
|
|
301ac781d6 | ||
|
|
ce3c98ae9b | ||
|
|
1c3a6ffbde | ||
|
|
2447b3bea7 | ||
|
|
63888ff70e | ||
|
|
c6e3d1bd47 | ||
|
|
e03e02d702 | ||
|
|
1abf51ae2b | ||
|
|
c41dfa3185 | ||
|
|
52a07da230 | ||
|
|
f572bb1637 | ||
|
|
773ede4b6c | ||
|
|
e4d42c8bac | ||
|
|
5b85067e15 | ||
|
|
0d111f3a79 | ||
|
|
f6b6f2b4b7 | ||
|
|
d55a2aa04d | ||
|
|
0185eea740 | ||
|
|
d44e8874b8 | ||
|
|
e34d33dd61 | ||
|
|
4b2fb80c23 | ||
|
|
acbac95818 | ||
|
|
3c908ac079 | ||
|
|
563f068948 | ||
|
|
6bf4300372 | ||
|
|
1344b958cb | ||
|
|
32d7cab70c | ||
|
|
6f8f6d47f8 | ||
|
|
04a48979eb | ||
|
|
1293fa2dec | ||
|
|
ff3a8fa738 | ||
|
|
8542cbf37e | ||
|
|
62c6fba9a0 | ||
|
|
2d84154226 | ||
|
|
c4e306d569 | ||
|
|
83b6c6de1c | ||
|
|
e86f7491ef | ||
|
|
5151d83af9 | ||
|
|
e462cb0596 | ||
|
|
a30c0e4608 | ||
|
|
dbe8c1b9e6 | ||
|
|
f90e7982f7 | ||
|
|
40c69d7cba | ||
|
|
c9e6f3e532 | ||
|
|
23ee50f688 | ||
|
|
a7c857fb71 | ||
|
|
0f6622360c | ||
|
|
578bc6e546 | ||
|
|
70fd038207 | ||
|
|
b30110a48a | ||
|
|
21978c4c1d | ||
|
|
ec48f2350d | ||
|
|
fe1e3bb847 | ||
|
|
69776f26d8 | ||
|
|
ab4092c088 | ||
|
|
4948a8d7de | ||
|
|
0a607bce18 | ||
|
|
e59820e3d6 | ||
|
|
85a2d25c1b | ||
|
|
3bc7fc84f9 | ||
|
|
249ce1ecbb | ||
|
|
be7f8d9916 | ||
|
|
0a0495f1a7 | ||
|
|
2f7afa8556 | ||
|
|
2f40905372 | ||
|
|
a6d0177153 | ||
|
|
b9e24888e7 | ||
|
|
976199e5c6 | ||
|
|
9017dae664 | ||
|
|
94acc241d9 | ||
|
|
765f3a476d | ||
|
|
449287e25a | ||
|
|
db2669d699 | ||
|
|
2cdb916233 | ||
|
|
5f14261aba | ||
|
|
5ae7855c7f | ||
|
|
20138505c4 | ||
|
|
49c09bfd85 | ||
|
|
d8ed47ddb8 | ||
|
|
39bfc41d33 | ||
|
|
68a8ac588f | ||
|
|
49d9ece15a | ||
|
|
475edf6732 | ||
|
|
76ea89b668 | ||
|
|
56b3a8df6b | ||
|
|
e02de0c0f6 | ||
|
|
84bfab2a72 | ||
|
|
a6825a2df2 | ||
|
|
f9dc539057 | ||
|
|
833b40bdc3 | ||
|
|
9ba58d1a46 | ||
|
|
29a99d3f97 | ||
|
|
a8b6fe78ba | ||
|
|
4abd69b460 | ||
|
|
0ca1d82861 | ||
|
|
3dd25b9f83 | ||
|
|
5c55b49976 | ||
|
|
c932961dd4 | ||
|
|
20cd5bef16 | ||
|
|
9b058bb17f | ||
|
|
47357c3e25 | ||
|
|
3ec5a1cf2c | ||
|
|
13d8a7baf7 | ||
|
|
19b1e74d06 | ||
|
|
82de9b9b94 | ||
|
|
2325f4e31c | ||
|
|
867af58695 | ||
|
|
cdd50f0258 | ||
|
|
8c8b92cc4e | ||
|
|
c0a3df0aa8 | ||
|
|
31dd1619f6 | ||
|
|
ba6be6c84d | ||
|
|
b126b98eb0 | ||
|
|
afb010e3ae | ||
|
|
f14e3d0f5e | ||
|
|
ddebdcda43 | ||
|
|
29c69418e9 | ||
|
|
3e565e5195 | ||
|
|
a13878d4a8 | ||
|
|
1b352d6162 | ||
|
|
258aa77cdd | ||
|
|
bc8de87cda | ||
|
|
7789bd145c | ||
|
|
c1457cc83f | ||
|
|
3ca0089ee8 | ||
|
|
29c9476c16 | ||
|
|
9e72f51851 | ||
|
|
a349611a30 | ||
|
|
1cd21087c2 | ||
|
|
eecb8f9c05 | ||
|
|
e36fd28f47 | ||
|
|
7ce44f4d84 | ||
|
|
6d8e4e8ebc | ||
|
|
deb507859a | ||
|
|
a165675b2a | ||
|
|
8b316d0abf | ||
|
|
def5059ff1 | ||
|
|
e1febfae31 | ||
|
|
82aa506902 | ||
|
|
de1aa40ecf | ||
|
|
53724b437e | ||
|
|
1592620141 | ||
|
|
28ef87a95b | ||
|
|
d4019eea99 | ||
|
|
d13b46cbba |
22
.github/workflows/core.tests.yml
vendored
22
.github/workflows/core.tests.yml
vendored
@@ -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
|
||||
|
||||
5
.github/workflows/desktop.publish.yml
vendored
5
.github/workflows/desktop.publish.yml
vendored
@@ -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:
|
||||
|
||||
14
.github/workflows/desktop.tests.yml
vendored
14
.github/workflows/desktop.tests.yml
vendored
@@ -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: |
|
||||
|
||||
6
.github/workflows/editor.tests.yml
vendored
6
.github/workflows/editor.tests.yml
vendored
@@ -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"
|
||||
|
||||
4
.github/workflows/monograph.publish.yml
vendored
4
.github/workflows/monograph.publish.yml
vendored
@@ -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
|
||||
|
||||
|
||||
21
.github/workflows/web.tests.yml
vendored
21
.github/workflows/web.tests.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -12,3 +12,4 @@ site
|
||||
node_modules.backup
|
||||
.nx
|
||||
/*.patch
|
||||
.taskcache
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
24
apps/desktop/__tests__/global-setup.ts
Normal file
24
apps/desktop/__tests__/global-setup.ts
Normal 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();
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
64
apps/desktop/__tests__/test-override.ts
Normal file
64
apps/desktop/__tests__/test-override.ts
Normal 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 */
|
||||
});
|
||||
}
|
||||
@@ -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 =
|
||||
|
||||
@@ -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",
|
||||
|
||||
2091
apps/desktop/package-lock.json
generated
2091
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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, "..", "..", "..")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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_")) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
@@ -336,7 +336,6 @@ const AppLocked = () => {
|
||||
borderRadius: 150,
|
||||
marginBottom: 10
|
||||
}}
|
||||
fontSize={AppFontSize.md}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
78
apps/mobile/app/components/auth/header.tsx
Normal file
78
apps/mobile/app/components/auth/header.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
27
apps/mobile/app/components/auth/signup-context.ts
Normal file
27
apps/mobile/app/components/auth/signup-context.ts
Normal 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);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
@@ -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 />
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -135,7 +135,6 @@ const ResultDialog = () => {
|
||||
paddingHorizontal: DefaultAppStyles.GAP
|
||||
}}
|
||||
onPress={close}
|
||||
fontSize={AppFontSize.md + 2}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
88
apps/mobile/app/components/loading/index.tsx
Normal file
88
apps/mobile/app/components/loading/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1167
apps/mobile/app/components/paywall/index.tsx
Normal file
1167
apps/mobile/app/components/paywall/index.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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'
|
||||
}
|
||||
*/
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
490
apps/mobile/app/components/sheets/buy-plan/index.tsx
Normal file
490
apps/mobile/app/components/sheets/buy-plan/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
262
apps/mobile/app/components/sheets/paywall/index.tsx
Normal file
262
apps/mobile/app/components/sheets/paywall/index.tsx
Normal 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} />
|
||||
});
|
||||
};
|
||||
130
apps/mobile/app/components/sheets/plan-limits/index.tsx
Normal file
130
apps/mobile/app/components/sheets/plan-limits/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} />,
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
104
apps/mobile/app/hooks/use-feature-manager.ts
Normal file
104
apps/mobile/app/hooks/use-feature-manager.ts
Normal 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;
|
||||
}
|
||||
@@ -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]);
|
||||
|
||||
|
||||
712
apps/mobile/app/hooks/use-pricing-plans.ts
Normal file
712
apps/mobile/app/hooks/use-pricing-plans.ts
Normal 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;
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user