mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 10:39:07 +02:00
Compare commits
419 Commits
fix/sqlite
...
feat/prici
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
169db9ba59 | ||
|
|
9c044d86d5 | ||
|
|
2e926015e3 | ||
|
|
2d43e734ca | ||
|
|
05df9b77ed | ||
|
|
69ba20009c | ||
|
|
9bf490bcb4 | ||
|
|
5a40080347 | ||
|
|
f42155ca5d | ||
|
|
3b06450113 | ||
|
|
2fb3c3a02b | ||
|
|
c8648659c7 | ||
|
|
83a663da2b | ||
|
|
5f0d14f01c | ||
|
|
069ec39de8 | ||
|
|
bef06a2a20 | ||
|
|
1a98fb8996 | ||
|
|
dc15bf75c6 | ||
|
|
7357422053 | ||
|
|
7918c089d3 | ||
|
|
ba50b75c54 | ||
|
|
0d7f32bdd0 | ||
|
|
8251e9437b | ||
|
|
416d5aef90 | ||
|
|
edb2e3a0ed | ||
|
|
64031a7814 | ||
|
|
1bd3c3d0f4 | ||
|
|
21b6fc4982 | ||
|
|
b881023e7b | ||
|
|
6387156596 | ||
|
|
2b2cfe9d33 | ||
|
|
e81580024e | ||
|
|
8b207b47dd | ||
|
|
15aab4ec29 | ||
|
|
bdb0474645 | ||
|
|
8d25d03390 | ||
|
|
ea2c7d2ea9 | ||
|
|
0ff38c7ca3 | ||
|
|
595f2e9ba7 | ||
|
|
b02bf3d4ad | ||
|
|
1e295d8562 | ||
|
|
27a0aeb170 | ||
|
|
7e355c3ecb | ||
|
|
69468487c2 | ||
|
|
0809b5b701 | ||
|
|
171f28f7c0 | ||
|
|
0a2af0ec7d | ||
|
|
c53354c368 | ||
|
|
be414f869c | ||
|
|
de40285e21 | ||
|
|
a7fa0bde00 | ||
|
|
19c2d2007f | ||
|
|
ecc9736b90 | ||
|
|
ffd022cfa6 | ||
|
|
89aba00aac | ||
|
|
bcc739aa57 | ||
|
|
fc1015b3fb | ||
|
|
f1fa32c7f5 | ||
|
|
a1e794c583 | ||
|
|
c787ef29d6 | ||
|
|
7c257cfbd2 | ||
|
|
99c77e6c5f | ||
|
|
f060c65bd5 | ||
|
|
22559cdf42 | ||
|
|
1fb86cb27b | ||
|
|
7f31066e8f | ||
|
|
50e418dc53 | ||
|
|
59739067b6 | ||
|
|
f100e157d3 | ||
|
|
870ce259f0 | ||
|
|
95b50eb5ca | ||
|
|
f243d13b81 | ||
|
|
749a13efbc | ||
|
|
275d5d7f04 | ||
|
|
54de071e85 | ||
|
|
9647e2513b | ||
|
|
239781eabd | ||
|
|
21573d6cc8 | ||
|
|
9b091f0bb0 | ||
|
|
e0beef745e | ||
|
|
2f2653d223 | ||
|
|
7458aa53ae | ||
|
|
f7db306c26 | ||
|
|
7e8d6f2041 | ||
|
|
d424a6c48f | ||
|
|
7293224514 | ||
|
|
7706cfed9a | ||
|
|
bbfe0b237d | ||
|
|
2c7cddd853 | ||
|
|
37c1d39c41 | ||
|
|
5d4960a26f | ||
|
|
78f07b23df | ||
|
|
7aeb047b50 | ||
|
|
0e3a661dd0 | ||
|
|
fba350ed1e | ||
|
|
76bfbc7316 | ||
|
|
7683d8fa5b | ||
|
|
3fe5352b56 | ||
|
|
a86d9a9c0c | ||
|
|
a68f74bc1b | ||
|
|
18310698f6 | ||
|
|
020244231f | ||
|
|
59085d7b19 | ||
|
|
c15410e168 | ||
|
|
2962f13457 | ||
|
|
bfc77fc9a4 | ||
|
|
7e88ec88ee | ||
|
|
9ef07f22af | ||
|
|
268343be3e | ||
|
|
956e5373d1 | ||
|
|
919fa9bdf1 | ||
|
|
e06192c5b4 | ||
|
|
b5be5e0b59 | ||
|
|
05695cfcc8 | ||
|
|
423e1c6816 | ||
|
|
6db1cf5cc1 | ||
|
|
2e4a87abd9 | ||
|
|
c26988134f | ||
|
|
bcffdd0f91 | ||
|
|
de1051d52d | ||
|
|
b1a360e445 | ||
|
|
49e85dfa87 | ||
|
|
452ff12071 | ||
|
|
1b6fc40633 | ||
|
|
82b28f8263 | ||
|
|
6e47eae30a | ||
|
|
4040f2ecc4 | ||
|
|
2293866c61 | ||
|
|
2cb4a10076 | ||
|
|
2f7b8d3377 | ||
|
|
983220b2f2 | ||
|
|
ca2848e491 | ||
|
|
48e5c4d908 | ||
|
|
5774dd8c60 | ||
|
|
9d65184f42 | ||
|
|
395b2750e8 | ||
|
|
3bfb1d57a5 | ||
|
|
e59b432007 | ||
|
|
efbc566386 | ||
|
|
2576f1dd00 | ||
|
|
7d9388bd27 | ||
|
|
1979cd2ea4 | ||
|
|
7bd5dfb772 | ||
|
|
ac7c88218b | ||
|
|
fb642233c8 | ||
|
|
6e2a19d7e0 | ||
|
|
4d4993c570 | ||
|
|
916e097cb3 | ||
|
|
6def76e1bb | ||
|
|
d93ec27c9e | ||
|
|
15329597a5 | ||
|
|
49caf23436 | ||
|
|
e6d8401c56 | ||
|
|
11a5e8c180 | ||
|
|
5bd7a081c1 | ||
|
|
a740a5db81 | ||
|
|
a61f2efacf | ||
|
|
51a253f3e2 | ||
|
|
19ed753079 | ||
|
|
4a85c49f5d | ||
|
|
16dd4b3359 | ||
|
|
9279c671ac | ||
|
|
225b21977e | ||
|
|
026b9212c4 | ||
|
|
5ce59c6125 | ||
|
|
b7c81a956c | ||
|
|
873f976fd3 | ||
|
|
7d8a16c463 | ||
|
|
f5841985fa | ||
|
|
da86cbbe32 | ||
|
|
ed115d3f9e | ||
|
|
e52e74523a | ||
|
|
7e43c3b373 | ||
|
|
e6db186b62 | ||
|
|
998e62ade7 | ||
|
|
ac16bc74fc | ||
|
|
2aee26b082 | ||
|
|
a188920d96 | ||
|
|
1f3defc28c | ||
|
|
7aeeabfb3a | ||
|
|
6641d3e625 | ||
|
|
72031617eb | ||
|
|
78090b700b | ||
|
|
f2f8d81b98 | ||
|
|
84d68ca67b | ||
|
|
6368e47712 | ||
|
|
0f17b01a74 | ||
|
|
025e23356a | ||
|
|
ac85002dff | ||
|
|
3a23e4fbaf | ||
|
|
3547663af8 | ||
|
|
c5dfc02085 | ||
|
|
bb9a6431bf | ||
|
|
624d3d7ce8 | ||
|
|
170b0aa49a | ||
|
|
07cfd74506 | ||
|
|
1d40054ac0 | ||
|
|
5cd37f4b64 | ||
|
|
25675f71e8 | ||
|
|
202534fca9 | ||
|
|
a57558d8ab | ||
|
|
f1c201dca9 | ||
|
|
a73bff83fa | ||
|
|
e4428299bc | ||
|
|
487567ffbc | ||
|
|
efd83e6de3 | ||
|
|
a07230f707 | ||
|
|
35a2e11afd | ||
|
|
cf4a7a3d34 | ||
|
|
86e661ccd6 | ||
|
|
e7be9b35ed | ||
|
|
53a367b0b2 | ||
|
|
b6206e2ca7 | ||
|
|
a910c819f2 | ||
|
|
6dd7a4960f | ||
|
|
c4e15b8f90 | ||
|
|
f458a4b86a | ||
|
|
8a44a7b99a | ||
|
|
25065c01a0 | ||
|
|
87ae9e86e7 | ||
|
|
782c53b622 | ||
|
|
cb52b390ab | ||
|
|
5a49941003 | ||
|
|
6797b9b621 | ||
|
|
031a1b31b4 | ||
|
|
4e5762be45 | ||
|
|
7a6ba5d1d1 | ||
|
|
cc3ccd041d | ||
|
|
31453dc098 | ||
|
|
2e84099c80 | ||
|
|
5bbc0cd452 | ||
|
|
22aae401ba | ||
|
|
97f135cd07 | ||
|
|
976f933233 | ||
|
|
95fe1044d6 | ||
|
|
677201e979 | ||
|
|
6ec9370afb | ||
|
|
bea0f1ab0e | ||
|
|
5357be4741 | ||
|
|
02fdec74ce | ||
|
|
dbc907ceeb | ||
|
|
7bb53a5e0c | ||
|
|
69cb24149d | ||
|
|
438d412ae9 | ||
|
|
1b71edef45 | ||
|
|
a4c0e8412b | ||
|
|
fbcb8675ca | ||
|
|
fbf5c82d04 | ||
|
|
fa40c539ed | ||
|
|
f3218fdc31 | ||
|
|
d12ed967e3 | ||
|
|
b6021dc76b | ||
|
|
f95442a828 | ||
|
|
846aae312e | ||
|
|
68fdbcb7fc | ||
|
|
b938c58b67 | ||
|
|
759fba06ae | ||
|
|
10e24ae632 | ||
|
|
341032c884 | ||
|
|
aa1fa0ca81 | ||
|
|
398432efde | ||
|
|
57ddaafc16 | ||
|
|
e4a755c69f | ||
|
|
053f70cdb4 | ||
|
|
cdcdc43cad | ||
|
|
f1111191b1 | ||
|
|
529e2a4dfd | ||
|
|
d88d07a2b0 | ||
|
|
197ff36c20 | ||
|
|
d94474de79 | ||
|
|
fcf0e0c531 | ||
|
|
24048ebfbe | ||
|
|
5ae8cdd5b9 | ||
|
|
74187b335f | ||
|
|
16fc689299 | ||
|
|
578a9de680 | ||
|
|
62d475b2ef | ||
|
|
69c902c6c1 | ||
|
|
c8c1028cbc | ||
|
|
313807c384 | ||
|
|
5dc0e15094 | ||
|
|
12c5b6d227 | ||
|
|
364ae51e36 | ||
|
|
18947e0c93 | ||
|
|
3880db4339 | ||
|
|
ae5b0b6fcc | ||
|
|
2c729f90cc | ||
|
|
800514d769 | ||
|
|
b03b9482dc | ||
|
|
9019490dbd | ||
|
|
4a3e23c288 | ||
|
|
1800ebea2d | ||
|
|
a708c9e9eb | ||
|
|
2aeb875fec | ||
|
|
a8164e9f6c | ||
|
|
0ee8e738e5 | ||
|
|
8fdef2ac7d | ||
|
|
920b1d91f3 | ||
|
|
0ad382aced | ||
|
|
c8aa8e37cf | ||
|
|
9a37f7c506 | ||
|
|
717750d5d4 | ||
|
|
1274ecc3b5 | ||
|
|
4c2ad4567c | ||
|
|
ebc01af34a | ||
|
|
e66d8eee13 | ||
|
|
55e6a4b65a | ||
|
|
0aa1db584d | ||
|
|
a0e6266d28 | ||
|
|
bd0aaed863 | ||
|
|
9410c42f29 | ||
|
|
80186c1812 | ||
|
|
37dfda5c54 | ||
|
|
ec83517dc5 | ||
|
|
2eb4911592 | ||
|
|
cd0a6a8fcb | ||
|
|
b15f192244 | ||
|
|
406cb663c9 | ||
|
|
301a30a50a | ||
|
|
81e3432143 | ||
|
|
c43c2936ac | ||
|
|
fdea1807d2 | ||
|
|
5439578fcf | ||
|
|
e05e52f870 | ||
|
|
1c6243d12e | ||
|
|
a51dfb5b6e | ||
|
|
c98bddd35b | ||
|
|
1eb80ccc88 | ||
|
|
5a346aa7de | ||
|
|
910095a769 | ||
|
|
119059befa | ||
|
|
a8f5dea315 | ||
|
|
ca0a4cb02c | ||
|
|
a04cd6b8dc | ||
|
|
3e4ab0f6df | ||
|
|
a7fe1aca44 | ||
|
|
ff0188f602 | ||
|
|
c846f8abc8 | ||
|
|
8d8acfb99d | ||
|
|
3a5b5772ad | ||
|
|
8046932e4e | ||
|
|
c45aa84739 | ||
|
|
d98be849e3 | ||
|
|
727ac6d907 | ||
|
|
8302f8ba4c | ||
|
|
4db8b0db01 | ||
|
|
3f1614ab9a | ||
|
|
909d8c35f8 | ||
|
|
a73780f0b0 | ||
|
|
286fdbeec0 | ||
|
|
59099552f8 | ||
|
|
0b6013be58 | ||
|
|
2d93a17f34 | ||
|
|
5ef7e39e17 | ||
|
|
f7de4c9084 | ||
|
|
0672c864f7 | ||
|
|
cb3706c150 | ||
|
|
08c6a99336 | ||
|
|
adf961fa08 | ||
|
|
0eb9ae47e9 | ||
|
|
aa706300a4 | ||
|
|
fd183a0e1e | ||
|
|
b27cd4be0f | ||
|
|
9575561832 | ||
|
|
02f96f8d14 | ||
|
|
3cf46e7ad1 | ||
|
|
93551aa0b7 | ||
|
|
d00c35bdaf | ||
|
|
6e6b793568 | ||
|
|
9e064d88c6 | ||
|
|
1658ad8d18 | ||
|
|
c94da3bdf0 | ||
|
|
1f0fc83a7c | ||
|
|
a6af92c330 | ||
|
|
ef93ecc481 | ||
|
|
039709822c | ||
|
|
25c861ce77 | ||
|
|
670f1570b5 | ||
|
|
c93d82d2d6 | ||
|
|
bced20031b | ||
|
|
f8bd089b51 | ||
|
|
2a4d04a26f | ||
|
|
a0c32191f9 | ||
|
|
662d59f60a | ||
|
|
13befe1947 | ||
|
|
21f8b6d557 | ||
|
|
aef53cace9 | ||
|
|
382b5b0240 | ||
|
|
a45b66d449 | ||
|
|
dd67b1a803 | ||
|
|
00568ae839 | ||
|
|
2b2ea5717c | ||
|
|
5672de8565 | ||
|
|
1c70ad8cec | ||
|
|
0c344a2146 | ||
|
|
38f69b514c | ||
|
|
b502f6a08f | ||
|
|
d68c3269d3 | ||
|
|
6d9af85e2a | ||
|
|
1d58cfea3c | ||
|
|
c1fa4d3400 | ||
|
|
df179056d6 | ||
|
|
26f9e08c23 | ||
|
|
f719bebe5a | ||
|
|
9693c8ea54 | ||
|
|
040a677895 | ||
|
|
df2708147e | ||
|
|
faba5f6385 | ||
|
|
eab5bccb99 | ||
|
|
ed399c0d0a | ||
|
|
3c7e962fe0 | ||
|
|
56614ef19d | ||
|
|
169318c549 | ||
|
|
bd6a1b9bff | ||
|
|
443d6625f9 | ||
|
|
b6f95ff7cd | ||
|
|
1b13da54a1 | ||
|
|
adf0f132fd | ||
|
|
73ec04b42d |
@@ -15,6 +15,7 @@ const SCOPES = [
|
||||
"mobile",
|
||||
"web",
|
||||
"vericrypt",
|
||||
"monograph",
|
||||
"desktop",
|
||||
"crypto",
|
||||
"editor",
|
||||
|
||||
2
.github/workflows/android.publish.yml
vendored
2
.github/workflows/android.publish.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Use specific Java version for the builds
|
||||
uses: joschi/setup-jdk@v2
|
||||
with:
|
||||
java-version: "11"
|
||||
java-version: "17"
|
||||
architecture: "x64"
|
||||
|
||||
- name: Install node modules
|
||||
|
||||
83
.github/workflows/monograph.publish.yml
vendored
Normal file
83
.github/workflows/monograph.publish.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
# GitHub recommends pinning actions to a commit SHA.
|
||||
# To get a newer version, you will need to update the SHA.
|
||||
# You can also reference a tag or branch, but the action may change without warning.
|
||||
|
||||
name: Publish @notesnook/monograph
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
attestations: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: ./.github/actions/setup-node-with-cache
|
||||
|
||||
- name: Install packages
|
||||
run: |
|
||||
npm ci --ignore-scripts --prefer-offline --no-audit
|
||||
npm run bootstrap -- --scope=monograph
|
||||
|
||||
- name: Generate build
|
||||
run: npx nx build @notesnook/monograph
|
||||
|
||||
# Setup Buildx
|
||||
- name: Docker Setup Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
ecr: auto
|
||||
logout: true
|
||||
|
||||
# Pull previous image from docker hub to use it as cache to improve the image build time.
|
||||
- name: docker pull cache image
|
||||
continue-on-error: true
|
||||
run: docker pull streetwriters/monograph:latest
|
||||
|
||||
# Setup QEMU
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: streetwriters/monograph
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: apps/monograph
|
||||
file: apps/monograph/Dockerfile
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v8
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: streetwriters/monograph:latest
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-name: index.docker.io/streetwriters/monograph
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
@@ -40,12 +40,35 @@ Once you are inside the `./notesnook` directory, run the preparation step:
|
||||
npm install
|
||||
```
|
||||
|
||||
Now you can finally start the desktop app:
|
||||
Now you can finally start the desktop app for development:
|
||||
|
||||
```bash
|
||||
npm run start:desktop
|
||||
```
|
||||
|
||||
To run the app in release mode:
|
||||
|
||||
```bash
|
||||
npm run staging -- --rebuild
|
||||
```
|
||||
|
||||
This will compile and run the app in production mode but it won't generate any packages. To create the final packages, you'll have to run the following commands:
|
||||
|
||||
```bash
|
||||
npm run release -- --rebuild
|
||||
|
||||
# For macOS
|
||||
npx electron-builder --mac dmg --arm64 --x64 -publish never
|
||||
|
||||
# For Linux (AppImage)
|
||||
npx electron-builder --linux AppImage:x64 AppImage:arm64 -publish never
|
||||
|
||||
# For Windows
|
||||
npx electron-builder --win --publish never
|
||||
```
|
||||
|
||||
Feel free to play around with the `electron-builder` command to get the packages you need. `npx electron-builder --help` is a great resource to learn different commands & platforms supported by `electron-builder`.
|
||||
|
||||
## Developer guide
|
||||
|
||||
### The tech stack
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
owner: streetwriters
|
||||
repo: notesnook
|
||||
provider: github
|
||||
updaterCacheDirName: Notesnook
|
||||
2578
apps/desktop/package-lock.json
generated
2578
apps/desktop/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
"name": "@notesnook/desktop",
|
||||
"productName": "Notesnook",
|
||||
"description": "Your private note taking space",
|
||||
"version": "3.0.6",
|
||||
"version": "3.0.18",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -11,29 +11,29 @@
|
||||
"repository": "https://github.com/streetwriters/notesnook",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@notesnook/crypto": "file:../../packages/crypto",
|
||||
"@trpc/client": "10.38.3",
|
||||
"@trpc/server": "10.38.3",
|
||||
"better-sqlite3-multiple-ciphers": "^9.5.0",
|
||||
"electron-trpc": "0.5.2",
|
||||
"electron-updater": "6.1.4",
|
||||
"icojs": "^0.17.1",
|
||||
"sodium-native": "^4.1.1",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"better-sqlite3-multiple-ciphers": "11.2.1",
|
||||
"electron-trpc": "0.6.1",
|
||||
"electron-updater": "^6.3.4",
|
||||
"icojs": "^0.19.4",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.6.2",
|
||||
"zod": "^3.21.4"
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.16.1",
|
||||
"@types/yargs": "^17.0.24",
|
||||
"chokidar": "^3.5.3",
|
||||
"electron": "^29.3.1",
|
||||
"electron-builder": "^24.13.3",
|
||||
"esbuild": "^0.20.0",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"@types/node": "22.7.0",
|
||||
"@types/yargs": "^17.0.33",
|
||||
"chokidar": "^4.0.1",
|
||||
"electron": "^30.5.1",
|
||||
"electron-builder": "^25.0.5",
|
||||
"esbuild": "^0.24.0",
|
||||
"node-abi": "^3.68.0",
|
||||
"node-gyp-build": "^4.8.2",
|
||||
"prebuildify": "^6.0.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^6.14.1"
|
||||
"undici": "^6.19.8"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
@@ -158,7 +158,8 @@
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": true,
|
||||
"createDesktopShortcut": "always"
|
||||
"createDesktopShortcut": "always",
|
||||
"deleteAppDataOnUninstall": true
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
index fcb7f54..cb1c7f7 100644
|
||||
index 739a48c..7a7b4af 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
|
||||
@@ -117,6 +117,7 @@ class LinuxTargetHelper {
|
||||
StartupWMClass: appInfo.productName,
|
||||
...extra,
|
||||
...targetSpecificOptions.desktop,
|
||||
@@ -10,7 +10,7 @@ index fcb7f54..cb1c7f7 100644
|
||||
};
|
||||
const description = this.getDescription(targetSpecificOptions);
|
||||
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
||||
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
|
||||
@@ -160,6 +161,23 @@ class LinuxTargetHelper {
|
||||
data += `\n${name}=${desktopMeta[name]}`;
|
||||
}
|
||||
data += "\n";
|
||||
@@ -1,23 +0,0 @@
|
||||
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
|
||||
index 379cf3b..2644e5d 100644
|
||||
--- a/node_modules/electron-trpc/dist/main.mjs
|
||||
+++ b/node_modules/electron-trpc/dist/main.mjs
|
||||
@@ -221,9 +221,16 @@ class G {
|
||||
i(this, c).includes(r) || (i(this, c).push(r), I(this, T, W).call(this, r));
|
||||
}
|
||||
detachWindow(r) {
|
||||
+
|
||||
y(this, c, i(this, c).filter((n) => n !== r));
|
||||
- for (const [n, t] of i(this, u).entries())
|
||||
- n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||
+ for (const [n, t] of i(this, u).entries()) {
|
||||
+ try {
|
||||
+ n.startsWith(`${r.webContents.id}-`) && (t.unsubscribe(), i(this, u).delete(n));
|
||||
+ } catch(e) {
|
||||
+ console.error(e);
|
||||
+ // ignore
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
}
|
||||
c = new WeakMap(), u = new WeakMap(), T = new WeakSet(), W = function(r) {
|
||||
39
apps/desktop/patches/electron-trpc+0.6.1.patch
Normal file
39
apps/desktop/patches/electron-trpc+0.6.1.patch
Normal file
@@ -0,0 +1,39 @@
|
||||
diff --git a/node_modules/electron-trpc/dist/main.mjs b/node_modules/electron-trpc/dist/main.mjs
|
||||
index 07971db..b67a3cf 100644
|
||||
--- a/node_modules/electron-trpc/dist/main.mjs
|
||||
+++ b/node_modules/electron-trpc/dist/main.mjs
|
||||
@@ -537,8 +537,15 @@ class be {
|
||||
attachWindow(r) {
|
||||
F(this, w).includes(r) || (y("Attaching window", r.id), F(this, w).push(r), v(this, N, Z).call(this, r));
|
||||
}
|
||||
- detachWindow(r) {
|
||||
- y("Detaching window", r.id), L(this, w, F(this, w).filter((e) => e !== r)), v(this, R, M).call(this, { webContentsId: r.webContents.id });
|
||||
+ detachWindow(r, webContentsId) {
|
||||
+ y("Detaching window", r.id);
|
||||
+
|
||||
+ if (r.isDestroyed() && webContentsId === undefined) {
|
||||
+ throw new Error('webContentsId is required when calling detachWindow on a destroyed window');
|
||||
+ }
|
||||
+
|
||||
+ L(this, w, F(this, w).filter((e) => e !== r));
|
||||
+ v(this, R, M).call(this, { webContentsId: webContentsId ?? r.webContents.id });
|
||||
}
|
||||
}
|
||||
w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
@@ -548,6 +555,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
for (const [i, a] of F(this, E).entries())
|
||||
i.startsWith(`${r}-${e ?? ""}`) && (y("Closing subscription", i), a.unsubscribe(), F(this, E).delete(i));
|
||||
}, N = new WeakSet(), Z = function(r) {
|
||||
+ const webContentsId = r.webContents.id;
|
||||
r.webContents.on("did-start-navigation", ({ frame: e }) => {
|
||||
y(
|
||||
"Handling webContents `did-start-navigation` event",
|
||||
@@ -558,7 +566,7 @@ w = new WeakMap(), E = new WeakMap(), R = new WeakSet(), M = function({
|
||||
frameRoutingId: e.routingId
|
||||
});
|
||||
}), r.webContents.on("destroyed", () => {
|
||||
- y("Handling webContents `destroyed` event"), this.detachWindow(r);
|
||||
+ y("Handling webContents `destroyed` event"), this.detachWindow(r, webContentsId);
|
||||
});
|
||||
};
|
||||
const me = ({
|
||||
@@ -1,46 +0,0 @@
|
||||
diff --git a/node_modules/node-gyp-build/bin.js b/node_modules/node-gyp-build/bin.js
|
||||
index 3fbcdf0..7ca3ab5 100644
|
||||
--- a/node_modules/node-gyp-build/bin.js
|
||||
+++ b/node_modules/node-gyp-build/bin.js
|
||||
@@ -16,7 +16,8 @@ if (!buildFromSource()) {
|
||||
}
|
||||
|
||||
function build () {
|
||||
- var args = [os.platform() === 'win32' ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
|
||||
+ var win32 = os.platform() === 'win32'
|
||||
+ var args = [win32 ? 'node-gyp.cmd' : 'node-gyp', 'rebuild']
|
||||
|
||||
try {
|
||||
var pkg = require('node-gyp/package.json')
|
||||
@@ -27,7 +28,7 @@ function build () {
|
||||
]
|
||||
} catch (_) {}
|
||||
|
||||
- proc.spawn(args[0], args.slice(1), { stdio: 'inherit' }).on('exit', function (code) {
|
||||
+ proc.spawn(args[0], args.slice(1), { stdio: 'inherit', shell: win32, windowsHide: true }).on('exit', function (code) {
|
||||
if (code || !process.argv[3]) process.exit(code)
|
||||
exec(process.argv[3]).on('exit', function (code) {
|
||||
process.exit(code)
|
||||
@@ -45,15 +46,18 @@ function preinstall () {
|
||||
|
||||
function exec (cmd) {
|
||||
if (process.platform !== 'win32') {
|
||||
- var shell = os.platform() === 'android' ? 'sh' : '/bin/sh'
|
||||
- return proc.spawn(shell, ['-c', '--', cmd], {
|
||||
+ var shell = os.platform() === 'android' ? 'sh' : true
|
||||
+ return proc.spawn(cmd, [], {
|
||||
+ shell,
|
||||
stdio: 'inherit'
|
||||
})
|
||||
}
|
||||
|
||||
- return proc.spawn(process.env.comspec || 'cmd.exe', ['/s', '/c', '"' + cmd + '"'], {
|
||||
+ return proc.spawn(cmd, [], {
|
||||
windowsVerbatimArguments: true,
|
||||
- stdio: 'inherit'
|
||||
+ stdio: 'inherit',
|
||||
+ shell: true,
|
||||
+ windowsHide: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -46,11 +46,11 @@ if (args.rebuild || !existsSync(path.join(webAppPath, "build"))) {
|
||||
// temporary until there's support for prebuilt binaries for linux ARM
|
||||
if (os.platform() === "linux") await patchBetterSQLite3();
|
||||
|
||||
if (os.platform() === "win32")
|
||||
await exec(
|
||||
`npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||
path.join(__dirname, "..", "node_modules", "sodium-native")
|
||||
);
|
||||
// if (os.platform() === "win32")
|
||||
// await exec(
|
||||
// `npx prebuildify --arch=arm64 --strip -t electron@${packageJson.devDependencies.electron}`,
|
||||
// path.join(__dirname, "..", "node_modules", "sodium-native")
|
||||
// );
|
||||
|
||||
await fs.cp(path.join(webAppPath, "build"), "build", {
|
||||
recursive: true,
|
||||
@@ -94,7 +94,7 @@ async function patchBetterSQLite3() {
|
||||
);
|
||||
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
|
||||
|
||||
json.version = "9.5.1";
|
||||
json.version = "11.2.2";
|
||||
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
|
||||
json.repository.url =
|
||||
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { initTRPC } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { app, dialog, nativeTheme, Notification, shell } from "electron";
|
||||
import { AutoLaunch } from "../utils/autolaunch";
|
||||
import { config, DesktopIntegration } from "../utils/config";
|
||||
import { bringToFront } from "../utils/bring-to-front";
|
||||
@@ -152,6 +152,10 @@ export const osIntegrationRouter = t.router({
|
||||
await rm(input);
|
||||
}),
|
||||
|
||||
restart: t.procedure.query(() => {
|
||||
app.relaunch();
|
||||
app.exit();
|
||||
}),
|
||||
showNotification: t.procedure
|
||||
.input(NotificationOptions)
|
||||
.query(({ input }) => {
|
||||
@@ -191,7 +195,10 @@ export const osIntegrationRouter = t.router({
|
||||
({ input: { theme, windowControlsIconColor, backgroundColor } }) => {
|
||||
if (windowControlsIconColor) {
|
||||
config.windowControlsIconColor = windowControlsIconColor;
|
||||
if (process.platform === "win32")
|
||||
if (
|
||||
process.platform === "win32" &&
|
||||
!config.desktopSettings.nativeTitlebar
|
||||
)
|
||||
globalThis.window?.setTitleBarOverlay({
|
||||
symbolColor: windowControlsIconColor
|
||||
});
|
||||
|
||||
143
apps/desktop/src/api/sqlite-kysely.ts
Normal file
143
apps/desktop/src/api/sqlite-kysely.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
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 type { Database, Statement } from "better-sqlite3-multiple-ciphers";
|
||||
import type { QueryResult } from "@streetwriters/kysely";
|
||||
|
||||
type SQLiteCompatibleType =
|
||||
| number
|
||||
| string
|
||||
| Uint8Array
|
||||
| Array<number>
|
||||
| bigint
|
||||
| null;
|
||||
|
||||
export class SQLite {
|
||||
sqlite?: Database;
|
||||
initialized = false;
|
||||
preparedStatements: Map<string, Statement<unknown[]>> = new Map();
|
||||
retryCounter: Record<string, number> = {};
|
||||
constructor() {
|
||||
console.log("new sqlite worker");
|
||||
}
|
||||
|
||||
async open(filePath: string) {
|
||||
if (this.sqlite) {
|
||||
console.error("Database is already initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
this.sqlite = require("better-sqlite3-multiple-ciphers")(
|
||||
filePath
|
||||
).unsafeMode(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper function for preparing SQL statements with caching
|
||||
* to avoid unnecessary computations.
|
||||
*/
|
||||
async prepare(sql: string): Promise<Statement | undefined> {
|
||||
if (!this.sqlite) throw new Error("Database is not initialized.");
|
||||
try {
|
||||
const cached = this.preparedStatements.get(sql);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const prepared = this.sqlite.prepare(sql);
|
||||
if (!prepared) return;
|
||||
|
||||
this.preparedStatements.set(sql, prepared);
|
||||
|
||||
// reset retry count on success
|
||||
this.retryCounter[sql] = 0;
|
||||
return prepared;
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
|
||||
// statement prepare process can be flaky so retry at least 5 times
|
||||
// before giving up.
|
||||
if (this.retryCounter[sql] < 5) {
|
||||
this.retryCounter[sql] = (this.retryCounter[sql] || 0) + 1;
|
||||
console.warn("Failed to prepare statement. Retrying:", sql);
|
||||
return this.prepare(sql);
|
||||
} else this.retryCounter[sql] = 0;
|
||||
|
||||
if (ex instanceof Error) ex.message += ` (query: ${sql})`;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
async exec<R>(
|
||||
sql: string,
|
||||
parameters: SQLiteCompatibleType[] = []
|
||||
): Promise<QueryResult<R>> {
|
||||
const prepared = await this.prepare(sql);
|
||||
if (!prepared) return { rows: [] };
|
||||
try {
|
||||
if (prepared.reader) {
|
||||
return {
|
||||
rows: prepared.all(parameters) as R[]
|
||||
};
|
||||
} else {
|
||||
const { changes, lastInsertRowid } = prepared.run(parameters);
|
||||
const numAffectedRows =
|
||||
changes !== undefined && changes !== null && !isNaN(changes)
|
||||
? BigInt(changes)
|
||||
: undefined;
|
||||
return {
|
||||
numAffectedRows,
|
||||
insertId:
|
||||
lastInsertRowid !== undefined && lastInsertRowid !== null
|
||||
? typeof lastInsertRowid === "bigint"
|
||||
? lastInsertRowid
|
||||
: BigInt(lastInsertRowid)
|
||||
: undefined,
|
||||
rows: [] as R[]
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Error) e.message += ` (query: ${sql})`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async run<R>(
|
||||
sql: string,
|
||||
parameters?: SQLiteCompatibleType[]
|
||||
): Promise<QueryResult<R>> {
|
||||
if (!this.sqlite) throw new Error("No database is not opened.");
|
||||
return await this.exec(sql, parameters);
|
||||
}
|
||||
|
||||
async close() {
|
||||
if (!this.sqlite) return;
|
||||
|
||||
this.preparedStatements.clear();
|
||||
this.sqlite.close();
|
||||
this.sqlite = undefined;
|
||||
}
|
||||
|
||||
async delete(filePath: string) {
|
||||
await this.close();
|
||||
await require("fs/promises").rm(filePath, {
|
||||
force: true,
|
||||
maxRetries: 5,
|
||||
retryDelay: 500
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -29,19 +29,37 @@ type Progress = { percent: number };
|
||||
|
||||
const t = initTRPC.create();
|
||||
let cancellationToken: CancellationToken | undefined = undefined;
|
||||
let downloadTimeout: NodeJS.Timeout | undefined = undefined;
|
||||
|
||||
export const updaterRouter = t.router({
|
||||
autoUpdates: t.procedure.query(() => config.automaticUpdates),
|
||||
install: t.procedure.query(() => autoUpdater.quitAndInstall()),
|
||||
download: t.procedure.query(async () => {
|
||||
if (cancellationToken) return;
|
||||
cancellationToken = new CancellationToken();
|
||||
await autoUpdater
|
||||
.downloadUpdate(cancellationToken)
|
||||
.finally(() => (cancellationToken = undefined));
|
||||
clearTimeout(downloadTimeout);
|
||||
await new Promise<string[]>((resolve, reject) => {
|
||||
downloadTimeout = setTimeout(async () => {
|
||||
cancellationToken = new CancellationToken();
|
||||
autoUpdater.isUpdaterActive();
|
||||
await autoUpdater
|
||||
.downloadUpdate(cancellationToken)
|
||||
.then(resolve)
|
||||
.catch(reject)
|
||||
.finally(() => (cancellationToken = undefined));
|
||||
}, 1000);
|
||||
});
|
||||
}),
|
||||
check: t.procedure.query(async () => {
|
||||
await autoUpdater.checkForUpdates().catch(console.error);
|
||||
if (cancellationToken) return;
|
||||
clearTimeout(downloadTimeout);
|
||||
await new Promise<void>((resolve) => {
|
||||
downloadTimeout = setTimeout(async () => {
|
||||
await autoUpdater
|
||||
.checkForUpdates()
|
||||
.catch(console.error)
|
||||
.finally(resolve);
|
||||
}, 1000);
|
||||
});
|
||||
}),
|
||||
|
||||
toggleAutoUpdates: t.procedure
|
||||
@@ -61,7 +79,18 @@ export const updaterRouter = t.router({
|
||||
"update-not-available"
|
||||
),
|
||||
onAvailable: createSubscription<"update-available", UpdateInfo>(
|
||||
"update-available"
|
||||
"update-available",
|
||||
() => {
|
||||
if (!config.automaticUpdates) return false;
|
||||
autoUpdater.emit("download-progress", {
|
||||
bytesPerSecond: 0,
|
||||
delta: 0,
|
||||
percent: 0,
|
||||
total: 100,
|
||||
transferred: 0
|
||||
});
|
||||
return true;
|
||||
}
|
||||
),
|
||||
onError: createSubscription("error")
|
||||
});
|
||||
@@ -69,10 +98,11 @@ export const updaterRouter = t.router({
|
||||
function createSubscription<
|
||||
TName extends keyof AppUpdaterEvents,
|
||||
TReturnType = Parameters<AppUpdaterEvents[TName]>[0]
|
||||
>(eventName: TName) {
|
||||
>(eventName: TName, handler?: (args: TReturnType) => boolean) {
|
||||
return t.procedure.subscription(() => {
|
||||
return observable<TReturnType>((emit) => {
|
||||
const listener: AppUpdaterEvents[TName] = (...args: any[]) => {
|
||||
if (handler?.(args[0])) return;
|
||||
emit.next(args[0]);
|
||||
};
|
||||
autoUpdater.removeAllListeners(eventName);
|
||||
|
||||
@@ -21,3 +21,4 @@ export * from "./constants";
|
||||
export type { AppRouter } from "./api";
|
||||
export { type UpdateInfo } from "builder-util-runtime";
|
||||
export { type DesktopIntegration } from "./utils/config";
|
||||
export { SQLite } from "./api/sqlite-kysely.js";
|
||||
|
||||
@@ -84,22 +84,27 @@ async function createWindow() {
|
||||
format: process.platform === "win32" ? "ico" : "png"
|
||||
}),
|
||||
|
||||
titleBarStyle: "hidden",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
color: "#00000000",
|
||||
symbolColor: config.windowControlsIconColor
|
||||
},
|
||||
trafficLightPosition: {
|
||||
x: 16,
|
||||
y: 12
|
||||
},
|
||||
...(config.desktopSettings.nativeTitlebar
|
||||
? {}
|
||||
: {
|
||||
titleBarStyle: "hidden",
|
||||
frame: process.platform === "win32" || process.platform === "darwin",
|
||||
titleBarOverlay: {
|
||||
height: 37,
|
||||
color: "#00000000",
|
||||
symbolColor: config.windowControlsIconColor
|
||||
},
|
||||
trafficLightPosition: {
|
||||
x: 16,
|
||||
y: 12
|
||||
}
|
||||
}),
|
||||
|
||||
webPreferences: {
|
||||
zoomFactor: config.zoomFactor,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false,
|
||||
nodeIntegrationInWorker: true,
|
||||
spellcheck: config.isSpellCheckerEnabled,
|
||||
preload: __dirname + "/preload.js"
|
||||
}
|
||||
|
||||
@@ -20,16 +20,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { ELECTRON_TRPC_CHANNEL } from "electron-trpc/main";
|
||||
import { type RendererGlobalElectronTRPC } from "electron-trpc/src/types";
|
||||
import type { NNCrypto } from "@notesnook/crypto";
|
||||
// import type { NNCrypto } from "@notesnook/crypto";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { platform } from "os";
|
||||
import sqlite3, { Database } from "better-sqlite3-multiple-ciphers";
|
||||
|
||||
declare global {
|
||||
var os: () => "mas" | ReturnType<typeof platform>;
|
||||
var electronTRPC: RendererGlobalElectronTRPC;
|
||||
var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
var createSQLite3Database: (filename: string) => Database;
|
||||
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
@@ -42,6 +40,5 @@ process.once("loaded", async () => {
|
||||
globalThis.electronTRPC = electronTRPC;
|
||||
});
|
||||
|
||||
globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
|
||||
globalThis.createSQLite3Database = (filename) => sqlite3(filename);
|
||||
// globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
|
||||
globalThis.os = () => (MAC_APP_STORE ? "mas" : platform());
|
||||
|
||||
@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import { NativeImage, nativeImage } from "electron";
|
||||
import path from "path";
|
||||
import { isDevelopment } from "./index";
|
||||
import { parse, ParsedImage } from "icojs";
|
||||
import { ParsedImage, parseICO } from "icojs";
|
||||
import { getSystemTheme } from "./theme";
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
@@ -71,7 +71,7 @@ export class AssetManager {
|
||||
`${icon}${prefix}.ico`
|
||||
);
|
||||
const icoBuffer = await readFile(icoPath);
|
||||
const images = await parse(icoBuffer, "image/png");
|
||||
const images = await parseICO(icoBuffer, "image/png");
|
||||
ALL_ICONS.push({ id: icon, images, prefix });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ async function configureAutoUpdater() {
|
||||
autoUpdater.allowDowngrade = false;
|
||||
autoUpdater.allowPrerelease = false;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.disableWebInstaller = true;
|
||||
}
|
||||
|
||||
export { configureAutoUpdater };
|
||||
|
||||
@@ -25,7 +25,8 @@ export const DesktopIntegration = z.object({
|
||||
autoStart: z.boolean().optional(),
|
||||
startMinimized: z.boolean().optional(),
|
||||
minimizeToSystemTray: z.boolean().optional(),
|
||||
closeToSystemTray: z.boolean().optional()
|
||||
closeToSystemTray: z.boolean().optional(),
|
||||
nativeTitlebar: z.boolean().optional()
|
||||
});
|
||||
|
||||
export type DesktopIntegration = z.infer<typeof DesktopIntegration>;
|
||||
@@ -35,7 +36,8 @@ export const config = {
|
||||
autoStart: false,
|
||||
startMinimized: false,
|
||||
minimizeToSystemTray: false,
|
||||
closeToSystemTray: false
|
||||
closeToSystemTray: false,
|
||||
nativeTitlebar: false
|
||||
},
|
||||
privacyMode: false,
|
||||
isSpellCheckerEnabled: true,
|
||||
|
||||
2
apps/mobile/.gitignore
vendored
2
apps/mobile/.gitignore
vendored
@@ -38,7 +38,7 @@ DerivedData
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
*.hprof
|
||||
ios/.xcode.env.local
|
||||
**/.xcode.env.local
|
||||
# Android/IntelliJ
|
||||
#
|
||||
rn-build-deps/
|
||||
|
||||
@@ -25,7 +25,6 @@ import { I18nManager, View } from "react-native";
|
||||
import "react-native-gesture-handler";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { initializeLogger } from "./common/database/logger";
|
||||
import AppLockedOverlay from "./components/app-lock-overlay";
|
||||
import { withErrorBoundry } from "./components/exception-handler";
|
||||
import GlobalSafeAreaProvider from "./components/globalsafearea";
|
||||
@@ -41,35 +40,25 @@ import { useUserStore } from "./stores/use-user-store";
|
||||
I18nManager.allowRTL(false);
|
||||
I18nManager.forceRTL(false);
|
||||
I18nManager.swapLeftAndRightInRTL(false);
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
const App = () => {
|
||||
const init = useAppEvents();
|
||||
useAppEvents();
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
useEffect(() => {
|
||||
initializeLogger()
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
const { appLockEnabled, appLockMode } = SettingsService.get();
|
||||
if (appLockEnabled || appLockMode !== "none") {
|
||||
useUserStore.getState().lockApp(true);
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
globalThis["IS_MAIN_APP_RUNNING"] = true;
|
||||
init();
|
||||
setTimeout(async () => {
|
||||
SettingsService.onFirstLaunch();
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(true);
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
SettingsService.onFirstLaunch();
|
||||
setTimeout(async () => {
|
||||
await Notifications.get();
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(true);
|
||||
}
|
||||
TipManager.init();
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -1,156 +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 { Platform } from "react-native";
|
||||
import { MMKV } from "../database/mmkv";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
|
||||
const WEBSITE_ID = "3c6890ce-8410-49d5-8831-15fb2eb28a21";
|
||||
const baseUrl = "https://analytics.streetwriters.co/api/collect";
|
||||
|
||||
const UA =
|
||||
Platform.OS === "ios"
|
||||
? "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1"
|
||||
: `
|
||||
Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36`;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Routes} route
|
||||
* @param {Routes[]} conditions
|
||||
* @returns
|
||||
*/
|
||||
async function canUpdateAnalytics(route, conditions = []) {
|
||||
if (!useSettingStore?.getState()?.settings?.telemetry) return false;
|
||||
let eventsList = MMKV.getString("notesnookUserEvents");
|
||||
|
||||
if (eventsList) {
|
||||
eventsList = JSON.parse(eventsList);
|
||||
}
|
||||
|
||||
if (eventsList && eventsList[route]) {
|
||||
console.log("analytics: event already sent", route);
|
||||
return false;
|
||||
}
|
||||
if (route !== "/welcome") {
|
||||
for (let cond of conditions) {
|
||||
if (!eventsList || !eventsList[cond]) {
|
||||
console.log("analytics: conditions not met for event", route, cond);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("analytics: will send event", route);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function saveAnalytics(route, value = true) {
|
||||
let eventsList = MMKV.getString("notesnookUserEvents");
|
||||
if (eventsList) {
|
||||
eventsList = JSON.parse(eventsList);
|
||||
} else {
|
||||
eventsList = {};
|
||||
}
|
||||
eventsList[route] = value;
|
||||
MMKV.setString("notesnookUserEvents", JSON.stringify(eventsList));
|
||||
}
|
||||
|
||||
/**
|
||||
*@typedef {"/welcome" | "/home" | "/signup" | "/first-note" | "/account-created" | "/pro-sheet" | "/pro-plans" | "/iap-native" | "/pro-screen" | "/editor" | "/editor-toolbar" | "/properties" | "/sidemenu"} Routes
|
||||
* @param {Routes} prevRoute
|
||||
* @param {Routes} route
|
||||
* @param {Routes[]} conditions
|
||||
* @param {boolean} once
|
||||
* @returns
|
||||
*/
|
||||
|
||||
async function pageView(
|
||||
route,
|
||||
prevRoute = "",
|
||||
conditions = ["/welcome"],
|
||||
once = true
|
||||
) {
|
||||
if (__DEV__) return;
|
||||
if (!(await canUpdateAnalytics(route, conditions)) && once) return;
|
||||
let body = {
|
||||
payload: {
|
||||
website: WEBSITE_ID,
|
||||
url: `notesnook-${Platform.OS}${prevRoute}${route}`,
|
||||
referrer: `https://notesnook.com/notesnook-${Platform.OS}${prevRoute}`,
|
||||
hostname: `notesnook-${Platform.OS}`,
|
||||
language: "en-US",
|
||||
screen: "1920x1080"
|
||||
},
|
||||
type: "pageview"
|
||||
};
|
||||
|
||||
try {
|
||||
let response = await fetch(baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": UA
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
console.log("analytics: event sent", route);
|
||||
await saveAnalytics(route);
|
||||
return await response.text();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendEvent(type, value, once = true) {
|
||||
if (__DEV__) return;
|
||||
if (!(await canUpdateAnalytics(type)) && once) return;
|
||||
let body = {
|
||||
payload: {
|
||||
website: WEBSITE_ID,
|
||||
url: "/",
|
||||
event_type: type,
|
||||
event_value: value,
|
||||
hostname: "notesnook-android-app",
|
||||
language: "en-US",
|
||||
screen: "1920x1080"
|
||||
},
|
||||
type: "event"
|
||||
};
|
||||
|
||||
try {
|
||||
let response = await fetch(baseUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": UA
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
console.log(response.status);
|
||||
return await response.text();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
sendEvent,
|
||||
pageView,
|
||||
saveAnalytics
|
||||
};
|
||||
@@ -25,6 +25,7 @@ import { MMKVLoader, ProcessingModes } from "react-native-mmkv-storage";
|
||||
import { generateSecureRandom } from "react-native-securerandom";
|
||||
import { DatabaseLogger } from ".";
|
||||
import { MMKV } from "./mmkv";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
|
||||
// Database key cipher is persisted across different user sessions hence it has
|
||||
// it's independent storage which we will never clear. This is only used when application has
|
||||
@@ -208,7 +209,8 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
if (userKeyCredentials) {
|
||||
const userKeyCipher = await encrypt(
|
||||
{
|
||||
key: DB_KEY
|
||||
key: DB_KEY,
|
||||
salt: NOTESNOOK_DB_KEY_SALT
|
||||
},
|
||||
userKeyCredentials.password
|
||||
);
|
||||
@@ -221,6 +223,7 @@ export async function getDatabaseKey(appLockPassword) {
|
||||
|
||||
return DB_KEY;
|
||||
} catch (e) {
|
||||
ToastManager.error(e, "Error getting database key");
|
||||
console.log(e, "error");
|
||||
DatabaseLogger.error(e);
|
||||
return null;
|
||||
|
||||
@@ -18,39 +18,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import "./logger";
|
||||
import { database } from "@notesnook/common";
|
||||
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||
import { logger as dbLogger } from "@notesnook/core";
|
||||
import { Platform } from "react-native";
|
||||
import * as Gzip from "react-native-gzip";
|
||||
import EventSource from "../../utils/sse/even-source-ios";
|
||||
import AndroidEventSource from "../../utils/sse/event-source";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
} from "@streetwriters/kysely";
|
||||
import filesystem from "../filesystem";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
|
||||
database.host(
|
||||
__DEV__
|
||||
? {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
// API_HOST: "http://192.168.43.5:5264",
|
||||
// AUTH_HOST: "http://192.168.43.5:8264",
|
||||
// SSE_HOST: "http://192.168.43.5:7264",
|
||||
// SUBSCRIPTIONS_HOST: "http://192.168.43.5:9264",
|
||||
// ISSUES_HOST: "http://192.168.43.5:2624"
|
||||
}
|
||||
: {
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co"
|
||||
}
|
||||
);
|
||||
import SettingsService from "../../services/settings";
|
||||
|
||||
export async function setupDatabase(password) {
|
||||
const key = await getDatabaseKey(password);
|
||||
@@ -59,6 +41,15 @@ export async function setupDatabase(password) {
|
||||
|
||||
console.log("Opening database with key:", !!key);
|
||||
|
||||
database.host({
|
||||
API_HOST: "https://api.notesnook.com",
|
||||
AUTH_HOST: "https://auth.streetwriters.co",
|
||||
SSE_HOST: "https://events.streetwriters.co",
|
||||
SUBSCRIPTIONS_HOST: "https://subscriptions.streetwriters.co",
|
||||
ISSUES_HOST: "https://issues.streetwriters.co",
|
||||
...(SettingsService.getProperty("serverUrls") || {})
|
||||
});
|
||||
|
||||
database.setup({
|
||||
storage: Storage,
|
||||
eventsource: Platform.OS === "ios" ? EventSource : AndroidEventSource,
|
||||
|
||||
@@ -17,13 +17,19 @@ 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 { initialize } from "@notesnook/core/dist/logger";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import { initialize } from "@notesnook/core";
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
} from "@streetwriters/kysely";
|
||||
import { Platform } from "react-native";
|
||||
import { setLogger } from ".";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
|
||||
let loggerLoaded = false;
|
||||
const initializeLogger = async () => {
|
||||
if (loggerLoaded) return;
|
||||
await initialize({
|
||||
dialect: (name) => ({
|
||||
createDriver: () => {
|
||||
@@ -37,6 +43,7 @@ const initializeLogger = async () => {
|
||||
journalMode: Platform.OS === "ios" ? "DELETE" : "WAL"
|
||||
});
|
||||
setLogger();
|
||||
loggerLoaded = true;
|
||||
};
|
||||
|
||||
export { initializeLogger };
|
||||
|
||||
@@ -17,8 +17,12 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { DatabaseConnection, Driver, QueryResult } from "kysely";
|
||||
import { CompiledQuery } from "kysely";
|
||||
import type {
|
||||
DatabaseConnection,
|
||||
Driver,
|
||||
QueryResult
|
||||
} from "@streetwriters/kysely";
|
||||
import { CompiledQuery } from "@streetwriters/kysely";
|
||||
import { QuickSQLiteConnection, open } from "react-native-quick-sqlite";
|
||||
|
||||
type Config = { dbName: string; async: boolean; location: string };
|
||||
|
||||
@@ -21,15 +21,15 @@ import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import {
|
||||
decrypt,
|
||||
decryptMulti,
|
||||
deriveCryptoKey,
|
||||
encrypt,
|
||||
encryptMulti,
|
||||
generateCryptoKey,
|
||||
getCryptoKey,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
removeCryptoKey,
|
||||
decryptMulti,
|
||||
encryptMulti
|
||||
removeCryptoKey
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import { Dimensions, Image, Platform } from "react-native";
|
||||
import ImageResizer from "@bam.tech/react-native-image-resizer";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
|
||||
console.log("hello world");
|
||||
/**
|
||||
* Scale down & compress images to screen width
|
||||
* for loading in editor.
|
||||
|
||||
@@ -18,24 +18,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import {
|
||||
getFileNameWithExtension,
|
||||
isImage,
|
||||
isDocument
|
||||
} from "@notesnook/core/dist/utils/filename";
|
||||
import { getFileNameWithExtension } from "@notesnook/core";
|
||||
import React from "react";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { subscribe, zip } from "react-native-zip-archive";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ShareComponent } from "../../components/sheets/export-notes/share";
|
||||
import { ToastManager, presentSheet } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import Storage from "../database/storage";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
@@ -58,15 +54,10 @@ export async function downloadAllAttachments() {
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(
|
||||
attachments,
|
||||
onProgress,
|
||||
canceled,
|
||||
groupId
|
||||
) {
|
||||
export async function downloadAttachments(attachments) {
|
||||
await createCacheDir();
|
||||
if (!attachments || !attachments.length) return;
|
||||
const result = new Map();
|
||||
const groupId = `download-all-${Date.now()}`;
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
@@ -90,92 +81,117 @@ export async function downloadAttachments(
|
||||
|
||||
await RNFetchBlob.fs.mkdir(zipSourceFolder);
|
||||
|
||||
const isCancelled = () => {
|
||||
if (useAttachmentStore.getState().downloading[groupId]?.canceled) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId,
|
||||
current: 0,
|
||||
total: 0,
|
||||
success: false,
|
||||
message: "Download cancelled",
|
||||
canceled: true
|
||||
});
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
if (isCancelled()) return;
|
||||
let attachment = await db.attachments.attachment(attachments[i]);
|
||||
const hash = attachment.hash;
|
||||
try {
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
onProgress?.(
|
||||
i + 1 / attachments.length,
|
||||
`Downloading attachments (${i + 1}/${
|
||||
attachments.length
|
||||
})... Please wait`
|
||||
);
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: groupId,
|
||||
current: i + 1,
|
||||
total: attachments.length,
|
||||
filename: attachment.hash
|
||||
});
|
||||
// Download to cache
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
});
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCancelled()) return;
|
||||
|
||||
if (!uri) throw new Error("Failed to download file");
|
||||
// Move file to the source folder we will zip eventually and rename the file to it's actual name.
|
||||
const filePath = `${zipSourceFolder}/${attachment.filename}`;
|
||||
await RNFetchBlob.fs.mv(`${cacheDir}/${uri}`, filePath);
|
||||
result.set(hash, {
|
||||
filename: attachment.filename,
|
||||
status: FileDownloadStatus.Success,
|
||||
attachment: attachment
|
||||
});
|
||||
} catch (e) {
|
||||
result.set(hash, {
|
||||
filename: attachment.filename,
|
||||
status: FileDownloadStatus.Fail,
|
||||
reason: e
|
||||
});
|
||||
ToastManager.error(e, "Error downloading attachment");
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
if (result?.size) {
|
||||
let sub;
|
||||
try {
|
||||
onProgress?.(0, `Zipping... Please wait`);
|
||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||
|
||||
sub = subscribe(({ progress }) => {
|
||||
onProgress(
|
||||
progress,
|
||||
`Saving zip file (${(progress * 100).toFixed(1)}%)... Please wait`
|
||||
);
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: groupId,
|
||||
current: 0,
|
||||
total: 0,
|
||||
success: true
|
||||
});
|
||||
|
||||
if (isCancelled()) return;
|
||||
|
||||
let sub;
|
||||
try {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
current: 0,
|
||||
total: 1,
|
||||
message: "Saving zip file... Please wait",
|
||||
groupId
|
||||
});
|
||||
// If all goes well, zip the notesnook-attachments folder in cache.
|
||||
|
||||
sub = subscribe(({ progress }) => {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId,
|
||||
current: progress,
|
||||
total: 1,
|
||||
message: `Saving zip file (${(progress * 100).toFixed(
|
||||
1
|
||||
)}%)... Please wait`
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
sub?.remove();
|
||||
onProgress(1, `Saving zip file... Please wait`);
|
||||
if (Platform.OS === "android") {
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
outputFolder,
|
||||
`notesnook-attachments-${Date.now()}.zip`,
|
||||
"application/zip"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
}
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
sub?.remove();
|
||||
|
||||
onProgress?.(1, `Done`);
|
||||
releasePermissions(outputFolder);
|
||||
} catch (e) {
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
ToastManager.error(e, "Error zipping attachments");
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
// Move the zip to user selected directory.
|
||||
const file = await ScopedStorage.createFile(
|
||||
outputFolder,
|
||||
`notesnook-attachments-${Date.now()}.zip`,
|
||||
"application/zip"
|
||||
);
|
||||
await copyFileAsync(`file://${zipOutputFile}`, file.uri);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
current: 0,
|
||||
total: 0,
|
||||
message: undefined,
|
||||
success: true,
|
||||
groupId
|
||||
});
|
||||
releasePermissions(outputFolder);
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
current: 0,
|
||||
total: 0,
|
||||
message: undefined,
|
||||
success: false,
|
||||
groupId
|
||||
});
|
||||
releasePermissions(outputFolder);
|
||||
sub?.remove();
|
||||
ToastManager.error(e, "Error zipping attachments");
|
||||
}
|
||||
// Remove source & zip file from cache.
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
if (Platform.OS === "android") {
|
||||
RNFetchBlob.fs.unlink(zipOutputFile).catch(console.log);
|
||||
}
|
||||
}
|
||||
|
||||
export default async function downloadAttachment(
|
||||
@@ -211,7 +227,11 @@ export default async function downloadAttachment(
|
||||
try {
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(options.groupId || attachment.hash, attachment.hash);
|
||||
.downloadFile(
|
||||
options.groupId || attachment.hash,
|
||||
attachment.hash,
|
||||
attachment.chunkSize
|
||||
);
|
||||
if (!(await exists(attachment.hash))) {
|
||||
DatabaseLogger.log("Attachment does not exist after download.");
|
||||
return;
|
||||
@@ -259,15 +279,6 @@ export default async function downloadAttachment(
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
attachment.dateUploaded &&
|
||||
!isImage(attachment.mimeType) &&
|
||||
!isDocument(attachment.mimeType)
|
||||
) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
.catch(console.log);
|
||||
}
|
||||
if (Platform.OS === "ios" && !options.cache) {
|
||||
fileUri = folder.uri + `/${filename}`;
|
||||
}
|
||||
@@ -287,7 +298,6 @@ export default async function downloadAttachment(
|
||||
|
||||
return fileUri;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
@@ -296,6 +306,7 @@ export default async function downloadAttachment(
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}_dcache`)
|
||||
.catch(console.log);
|
||||
}
|
||||
DatabaseLogger.error(e);
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
if (options.throwError) {
|
||||
throw e;
|
||||
|
||||
@@ -17,32 +17,56 @@ 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 from "@notesnook/core/dist/utils/constants";
|
||||
import NetInfo from "@react-native-community/netinfo";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { cacheDir, fileCheck } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { ABYTES, cacheDir, getUploadedFileSize, parseS3Error } from "./utils";
|
||||
|
||||
export async function downloadFile(filename, data, cancelToken) {
|
||||
if (!data) {
|
||||
DatabaseLogger.log(`Error downloading file: ${filename}, reason: No data`);
|
||||
export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: No requestOptions`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
DatabaseLogger.log(`Downloading ${filename}`);
|
||||
await createCacheDir();
|
||||
let { url, headers } = data;
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
let { url, headers, chunkSize } = requestOptions;
|
||||
let tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
let originalFilePath = `${cacheDir}/${filename}`;
|
||||
try {
|
||||
if (await exists(filename)) {
|
||||
DatabaseLogger.log(`File Exists already: ${filename}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
const attachment = await db.attachments.attachment(filename);
|
||||
const size = await getUploadedFileSize(filename);
|
||||
|
||||
if (size === -1) {
|
||||
const error = `Uploaded file verification failed. (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
if (size === 0) {
|
||||
const error = `File length is 0. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(size / chunkSize);
|
||||
const decryptedLength = size - totalChunks * ABYTES;
|
||||
|
||||
if (attachment && attachment.size !== decryptedLength) {
|
||||
const error = `File length mismatch. Expected ${attachment.size} but got ${decryptedLength} bytes. Please upload this file again from the attachment manager. (File hash: ${filename})`;
|
||||
await db.attachments.markAsFailed(attachment.id, error);
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
let res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers
|
||||
@@ -63,34 +87,57 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
);
|
||||
throw new Error("Unable to resolve download url");
|
||||
}
|
||||
let totalSize = 0;
|
||||
|
||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
||||
let request = RNFetchBlob.config({
|
||||
path: path,
|
||||
IOSBackgroundTask: true
|
||||
path: tempFilePath,
|
||||
IOSBackgroundTask: true,
|
||||
overwrite: true
|
||||
})
|
||||
.fetch("GET", downloadUrl, null)
|
||||
.progress((recieved, total) => {
|
||||
.progress(async (recieved, total) => {
|
||||
useAttachmentStore
|
||||
.getState()
|
||||
.setProgress(0, total, filename, recieved, "download");
|
||||
totalSize = total;
|
||||
|
||||
DatabaseLogger.log(`Downloading: ${filename}, ${recieved}/${total}`);
|
||||
});
|
||||
|
||||
cancelToken.cancel = () => {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
request.cancel();
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
DatabaseLogger.log(`Download cancelled: ${filename}`);
|
||||
};
|
||||
|
||||
let response = await request;
|
||||
await fileCheck(response, totalSize);
|
||||
console.log(response.info().headers);
|
||||
|
||||
const contentType =
|
||||
response.info().headers?.["content-type"] ||
|
||||
response.info().headers?.["Content-Type"];
|
||||
|
||||
if (contentType === "application/xml") {
|
||||
const error = parseS3Error(await response.text());
|
||||
throw new Error(`[${error.Code}] ${error.Message}`);
|
||||
}
|
||||
|
||||
let status = response.info().status;
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
|
||||
if (exists(originalFilePath)) {
|
||||
await RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
}
|
||||
|
||||
await RNFetchBlob.fs.mv(tempFilePath, originalFilePath);
|
||||
|
||||
if (!(await exists(filename))) {
|
||||
throw new Error("File size mismatch");
|
||||
}
|
||||
|
||||
return status >= 200 && status < 300;
|
||||
} catch (e) {
|
||||
if (e.message !== "canceled") {
|
||||
if (e.message !== "canceled" && !e.message.includes("NoSuchKey")) {
|
||||
ToastManager.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
@@ -106,7 +153,8 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
DatabaseLogger.error(e, {
|
||||
url,
|
||||
headers
|
||||
@@ -115,37 +163,23 @@ export async function downloadFile(filename, data, cancelToken) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAttachment(hash) {
|
||||
const internetState = await NetInfo.fetch();
|
||||
const isInternetReachable =
|
||||
internetState.isConnected && internetState.isInternetReachable;
|
||||
if (!isInternetReachable) return { success: true };
|
||||
if (!isInternetReachable) return;
|
||||
const attachment = await db.attachments.attachment(hash);
|
||||
if (!attachment) return { failed: "Attachment not found." };
|
||||
|
||||
try {
|
||||
const size = await getUploadedFileSize(hash);
|
||||
console.log("File Size", size);
|
||||
if (size === -1) return { success: true };
|
||||
|
||||
if (size === 0) return { failed: "File length is 0." };
|
||||
if (size === 0)
|
||||
return {
|
||||
failed: `File length is 0. Please upload this file again from the attachment manager. (File hash: ${hash})`
|
||||
};
|
||||
} catch (e) {
|
||||
return { failed: e?.message };
|
||||
}
|
||||
|
||||
@@ -17,17 +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 { checkAttachment, downloadFile, getUploadedFileSize } from "./download";
|
||||
import { checkAttachment, downloadFile } from "./download";
|
||||
import {
|
||||
clearFileStorage,
|
||||
deleteFile,
|
||||
exists,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64
|
||||
hashBase64,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable } from "./utils";
|
||||
import { cancelable, getUploadedFileSize } from "./utils";
|
||||
|
||||
export default {
|
||||
readEncrypted,
|
||||
@@ -39,5 +44,10 @@ export default {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
checkAttachment
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
};
|
||||
|
||||
@@ -20,9 +20,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
import { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
@@ -158,7 +159,35 @@ export async function migrateFilesFromCache() {
|
||||
}
|
||||
}
|
||||
|
||||
const ABYTES = 17;
|
||||
export async function clearCache() {
|
||||
await RNFetchBlob.fs.unlink(cacheDir).catch(console.log);
|
||||
await createCacheDir();
|
||||
eSendEvent("cache-cleared");
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByPath(path) {
|
||||
await RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteCacheFileByName(name) {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupPath = `${iosAppGroup}/${name}`;
|
||||
await RNFetchBlob.fs.unlink(appGroupPath).catch(console.log);
|
||||
await RNFetchBlob.fs.unlink(`${cacheDir}/${name}`).catch(console.log);
|
||||
}
|
||||
|
||||
export async function deleteDCacheFiles() {
|
||||
const files = await RNFetchBlob.fs.ls(cacheDir);
|
||||
for (const file of files) {
|
||||
if (file.includes("_dcache")) {
|
||||
await RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
@@ -200,3 +229,29 @@ export async function exists(filename) {
|
||||
}
|
||||
return exists;
|
||||
}
|
||||
|
||||
export async function bulkExists(files) {
|
||||
const cacheFiles = await RNFetchBlob.fs.ls(cacheDir);
|
||||
let missingFiles = files.filter((file) => !cacheFiles.includes(file));
|
||||
|
||||
if (Platform.OS === "ios") {
|
||||
const iosAppGroup =
|
||||
Platform.OS === "ios"
|
||||
? await RNFetchBlob.fs.pathForAppGroup(IOS_APPGROUPID)
|
||||
: null;
|
||||
const appGroupFiles = await RNFetchBlob.fs.ls(iosAppGroup);
|
||||
missingFiles = missingFiles.filter((file) => !appGroupFiles.includes(file));
|
||||
}
|
||||
|
||||
return missingFiles;
|
||||
}
|
||||
|
||||
export async function getCacheSize() {
|
||||
const stat = await RNFetchBlob.fs.lstat(`file://` + cacheDir);
|
||||
let total = 0;
|
||||
console.log("Total files", stat.length);
|
||||
stat.forEach((s) => {
|
||||
total += parseInt(s.size);
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
@@ -17,31 +17,30 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { cacheDir } from "./utils";
|
||||
import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { createCacheDir } from "./io";
|
||||
import { getUploadedFileSize } from "./download";
|
||||
import { cacheDir, getUploadedFileSize } from "./utils";
|
||||
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
let { url, headers } = data;
|
||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) return false;
|
||||
let { url, headers } = requestOptions;
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
try {
|
||||
const uploadedFileSize = await getUploadedFileSize(filename);
|
||||
|
||||
if (uploadedFileSize === -1) {
|
||||
DatabaseLogger.log("Upload verification failed.");
|
||||
return false;
|
||||
const error = `Uploaded file verification failed. (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const isUploaded = uploadedFileSize !== 0;
|
||||
if (isUploaded) {
|
||||
if (uploadedFileSize !== 0) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
@@ -50,9 +49,15 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
method: "PUT",
|
||||
headers
|
||||
});
|
||||
if (!res.ok) throw new Error(`${res.status}: Unable to resolve upload url`);
|
||||
const uploadUrl = await res.text();
|
||||
if (!uploadUrl) throw new Error("Unable to resolve attachment upload url");
|
||||
|
||||
const uploadUrl = res.ok ? await res.text() : await res.json();
|
||||
|
||||
if (typeof uploadUrl !== "string") {
|
||||
throw new Error(
|
||||
uploadUrl.error || "Unable to resolve attachment upload url."
|
||||
);
|
||||
}
|
||||
|
||||
let uploadFilePath = `${cacheDir}/${filename}`;
|
||||
|
||||
const iosAppGroup =
|
||||
@@ -110,9 +115,6 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
);
|
||||
let attachment = await db.attachments.attachment(filename);
|
||||
if (!attachment) return result;
|
||||
if (!isImage(attachment.mimeType) && !isDocument(attachment.mimeType)) {
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
} else {
|
||||
const fileInfo = await RNFetchBlob.fs.stat(uploadFilePath);
|
||||
throw new Error(
|
||||
@@ -125,8 +127,10 @@ export async function uploadFile(filename, data, cancelToken) {
|
||||
return result;
|
||||
} catch (e) {
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
DatabaseLogger.info(`File upload error: ${filename}, ${e}`);
|
||||
DatabaseLogger.error(e);
|
||||
ToastManager.error(e, "File upload failed");
|
||||
DatabaseLogger.error(e, "File upload failed", {
|
||||
filename
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +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 * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { DatabaseLogger, db } from "../database";
|
||||
import { hosts } from "@notesnook/core";
|
||||
|
||||
export const ABYTES = 17;
|
||||
export const cacheDirOld = RNFetchBlob.fs.dirs.CacheDir;
|
||||
|
||||
export const cacheDir =
|
||||
@@ -34,24 +37,36 @@ export function getRandomId(prefix) {
|
||||
.replace("0.", prefix || "");
|
||||
}
|
||||
|
||||
export function extractValueFromXmlTag(code, xml) {
|
||||
if (!xml.includes(code)) return `Unknown ${code}`;
|
||||
return xml.slice(
|
||||
xml.indexOf(`<${code}>`) + code.length + 2,
|
||||
xml.indexOf(`</${code}>`)
|
||||
);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {string | undefined} data
|
||||
* @returns
|
||||
*/
|
||||
export function parseS3Error(data) {
|
||||
const xml = typeof data === "string" ? data : null;
|
||||
|
||||
export async function fileCheck(response, totalSize) {
|
||||
if (totalSize < 1000) {
|
||||
let text = await response.text();
|
||||
if (text.startsWith("<?xml")) {
|
||||
let errorJson = {
|
||||
Code: extractValueFromXmlTag("Code", text),
|
||||
Message: extractValueFromXmlTag("Message", text)
|
||||
};
|
||||
throw new Error(`${errorJson.Code}: ${errorJson.Message}`);
|
||||
const error = {
|
||||
Code: "UNKNOWN",
|
||||
Message: xml
|
||||
};
|
||||
|
||||
try {
|
||||
if (!xml) return error;
|
||||
const doc = new DOMParser().parseFromString(xml, "text/xml");
|
||||
|
||||
const ErrorElement = doc.getElementsByTagName("Error")[0];
|
||||
if (!ErrorElement) return error;
|
||||
|
||||
for (const child of ErrorElement.children) {
|
||||
if (
|
||||
child.textContent &&
|
||||
(child.tagName === "Code" || child.tagName === "Message")
|
||||
)
|
||||
error[child.tagName] = child.textContent;
|
||||
}
|
||||
return error;
|
||||
} catch (e) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,12 +74,11 @@ export function cancelable(operation) {
|
||||
const cancelToken = {
|
||||
cancel: () => {}
|
||||
};
|
||||
return (filename, { url, headers }) => {
|
||||
return (filename, requestOptions) => {
|
||||
return {
|
||||
execute: () => operation(filename, { url, headers }, cancelToken),
|
||||
execute: () => operation(filename, requestOptions, cancelToken),
|
||||
cancel: async () => {
|
||||
await cancelToken.cancel();
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -91,3 +105,21 @@ export async function releasePermissions(path) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUploadedFileSize(hash) {
|
||||
try {
|
||||
const url = `${hosts.API_HOST}/s3?name=${hash}`;
|
||||
const token = await db.tokenManager.getAccessToken();
|
||||
const attachmentInfo = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
const contentLength = parseInt(
|
||||
attachmentInfo.headers?.get("content-length")
|
||||
);
|
||||
return isNaN(contentLength) ? 0 : contentLength;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import { AppStateStatus, Platform, TextInput, View } from "react-native";
|
||||
import {
|
||||
AppStateStatus,
|
||||
Platform,
|
||||
TextInput,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
//@ts-ignore
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { enabled } from "react-native-privacy-snapshot";
|
||||
import { DatabaseLogger } from "../../common/database";
|
||||
import {
|
||||
decrypt,
|
||||
@@ -47,6 +52,7 @@ import Input from "../ui/input";
|
||||
import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
|
||||
const getUser = () => {
|
||||
const user = MMKV.getString("user");
|
||||
@@ -87,6 +93,7 @@ const AppLockedOverlay = () => {
|
||||
const appState = useAppState();
|
||||
const lastAppState = useRef<AppStateStatus>(appState);
|
||||
const biometricUnlockAwaitingUserInput = useRef(false);
|
||||
const { height } = useWindowDimensions();
|
||||
const keyboardType = useSettingStore(
|
||||
(state) => state.settings.applockKeyboardType
|
||||
);
|
||||
@@ -121,7 +128,6 @@ const AppLockedOverlay = () => {
|
||||
);
|
||||
if (unlocked) {
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
}
|
||||
biometricUnlockAwaitingUserInput.current = false;
|
||||
@@ -148,7 +154,6 @@ const AppLockedOverlay = () => {
|
||||
}
|
||||
|
||||
lockApp(false);
|
||||
enabled(false);
|
||||
password.current = undefined;
|
||||
} else {
|
||||
ToastManager.show({
|
||||
@@ -163,10 +168,6 @@ const AppLockedOverlay = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (appState === "active") {
|
||||
enabled(false);
|
||||
}
|
||||
|
||||
const prevState = lastAppState.current;
|
||||
lastAppState.current = appState;
|
||||
|
||||
@@ -192,26 +193,26 @@ const AppLockedOverlay = () => {
|
||||
DatabaseLogger.info("Biometric unlock request");
|
||||
onUnlockAppRequested();
|
||||
}
|
||||
|
||||
enabled(false);
|
||||
} else {
|
||||
SettingsService.appEnteredBackground();
|
||||
if (SettingsService.get().privacyScreen) {
|
||||
enabled(true);
|
||||
}
|
||||
}
|
||||
}, [appState, onUnlockAppRequested, appLocked]);
|
||||
|
||||
return appLocked ? (
|
||||
<View
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
justifyContent: "center"
|
||||
zIndex: 999
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
justifyContent: "center",
|
||||
minHeight: height
|
||||
}}
|
||||
keyboardDismissMode="interactive"
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Toast context="local" />
|
||||
<View
|
||||
@@ -326,7 +327,7 @@ const AppLockedOverlay = () => {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
) : null;
|
||||
};
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ import filesystem from "../../common/filesystem";
|
||||
import downloadAttachment from "../../common/filesystem/download-attachment";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import picker from "../../screens/editor/tiptap/picker";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import {
|
||||
ToastManager,
|
||||
eSendEvent,
|
||||
@@ -49,14 +51,12 @@ import { Dialog } from "../dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { openNote } from "../list-items/note/wrapper";
|
||||
import { DateMeta } from "../properties/date-meta";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { Notice } from "../ui/notice";
|
||||
import { Pressable } from "../ui/pressable";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
import { editorController } from "../../screens/editor/tiptap/utils";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
|
||||
const Actions = ({
|
||||
attachment,
|
||||
@@ -88,7 +88,7 @@ const Actions = ({
|
||||
useAttachmentStore.getState().remove(attachment.hash);
|
||||
}
|
||||
downloadAttachment(attachment.hash, false);
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
fwdRef.current?.hide();
|
||||
},
|
||||
icon: "download"
|
||||
},
|
||||
@@ -118,12 +118,14 @@ const Actions = ({
|
||||
setLoading({
|
||||
name: "Run file check"
|
||||
});
|
||||
const res = await filesystem.checkAttachment(attachment.hash);
|
||||
if (res.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||
setFailed(res.failed);
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
|
||||
if (result.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, result.failed);
|
||||
setFailed(result.failed);
|
||||
ToastManager.show({
|
||||
heading: "File check failed with error: " + res.failed,
|
||||
heading: "File check failed with error: " + result.failed,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -147,9 +149,8 @@ const Actions = ({
|
||||
},
|
||||
{
|
||||
name: "Rename",
|
||||
onPress: () => {
|
||||
onPress: async () => {
|
||||
presentDialog({
|
||||
context: contextId as any,
|
||||
input: true,
|
||||
title: "Rename file",
|
||||
paragraph: "Enter a new name for the file",
|
||||
@@ -217,6 +218,7 @@ const Actions = ({
|
||||
style={{
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
keyboardShouldPersistTaps="never"
|
||||
>
|
||||
<Dialog context={contextId} />
|
||||
<SheetProvider context={contextId} />
|
||||
|
||||
@@ -22,7 +22,6 @@ import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
@@ -44,7 +43,8 @@ export const AttachmentItem = ({
|
||||
setAttachments,
|
||||
pressable = true,
|
||||
hideWhenNotDownloading,
|
||||
context
|
||||
context,
|
||||
errorOnly
|
||||
}: {
|
||||
id: string | number;
|
||||
attachments?: VirtualizedGrouping<Attachment>;
|
||||
@@ -53,9 +53,9 @@ export const AttachmentItem = ({
|
||||
pressable?: boolean;
|
||||
hideWhenNotDownloading?: boolean;
|
||||
context?: string;
|
||||
errorOnly?: boolean;
|
||||
}) => {
|
||||
const [attachment] = useDBItem(id, "attachment", attachments);
|
||||
|
||||
const { colors } = useThemeColors();
|
||||
const [currentProgress, setCurrentProgress] = useAttachmentProgress(
|
||||
attachment,
|
||||
@@ -67,8 +67,7 @@ export const AttachmentItem = ({
|
||||
Actions.present(attachment, setAttachments, context);
|
||||
};
|
||||
|
||||
return hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.value) ? null : (
|
||||
return errorOnly && attachment && !attachment?.failed ? null : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={onPress}
|
||||
@@ -78,8 +77,6 @@ export const AttachmentItem = ({
|
||||
justifyContent: "space-between",
|
||||
padding: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
minHeight: 45
|
||||
}}
|
||||
>
|
||||
@@ -89,41 +86,46 @@ export const AttachmentItem = ({
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center"
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: -5
|
||||
marginLeft: -5,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.border,
|
||||
paddingHorizontal: 2,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 5
|
||||
}}
|
||||
>
|
||||
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
|
||||
|
||||
<Paragraph
|
||||
adjustsFontSizeToFit
|
||||
size={6}
|
||||
color={colors.static.white}
|
||||
size={8}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{
|
||||
position: "absolute"
|
||||
maxWidth: 30
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{getFileExtension(attachment.filename).toUpperCase()}
|
||||
{getFileExtension(attachment.filename).toUpperCase() ||
|
||||
attachment.mimeType.split("/")?.[1]?.toUpperCase()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
marginLeft: 10
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.sm - 1}
|
||||
size={SIZE.sm}
|
||||
style={{
|
||||
flexWrap: "wrap",
|
||||
marginBottom: 2.5
|
||||
flexWrap: "wrap"
|
||||
}}
|
||||
numberOfLines={1}
|
||||
lineBreakMode="middle"
|
||||
@@ -133,11 +135,8 @@ export const AttachmentItem = ({
|
||||
</Paragraph>
|
||||
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
{formatBytes(attachment.size)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}>
|
||||
File size: {formatBytes(attachment.size)}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -1,297 +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, { useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import {
|
||||
PresentSheetOptions,
|
||||
presentSheet
|
||||
} from "../../services/event-manager";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import { Attachment, VirtualizedGrouping } from "@notesnook/core";
|
||||
|
||||
const DownloadAttachments = ({
|
||||
close,
|
||||
attachments,
|
||||
isNote,
|
||||
update
|
||||
}: {
|
||||
attachments: VirtualizedGrouping<Attachment>;
|
||||
close?: ((ctx?: string | undefined) => void) | undefined;
|
||||
isNote?: boolean;
|
||||
update?: (props: PresentSheetOptions) => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
const [progress, setProgress] = useState({
|
||||
value: 0,
|
||||
statusText: "Download started... Please wait"
|
||||
});
|
||||
const [result, setResult] = useState(new Map());
|
||||
const canceled = useRef(false);
|
||||
const groupId = useRef<string>();
|
||||
|
||||
const onDownload = async () => {
|
||||
update?.({
|
||||
disableClosing: true
|
||||
} as PresentSheetOptions);
|
||||
setDownloading(true);
|
||||
canceled.current = false;
|
||||
groupId.current = Date.now().toString();
|
||||
const result = await downloadAttachments(
|
||||
await attachments.ids(),
|
||||
(progress: number, statusText: string) =>
|
||||
setProgress({ value: progress, statusText }),
|
||||
canceled,
|
||||
groupId.current
|
||||
);
|
||||
if (canceled.current) return;
|
||||
setResult(result || new Map());
|
||||
setDownloading(false);
|
||||
update?.({
|
||||
disableClosing: false
|
||||
} as PresentSheetOptions);
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
update?.({
|
||||
disableClosing: false
|
||||
} as PresentSheetOptions);
|
||||
canceled.current = true;
|
||||
if (!groupId.current) return;
|
||||
console.log(groupId.current, "canceling groupId downloads");
|
||||
await db.fs().cancel(groupId.current);
|
||||
setDownloading(false);
|
||||
setResult(new Map());
|
||||
groupId.current = undefined;
|
||||
};
|
||||
|
||||
const failedResults = () => {
|
||||
const results = [];
|
||||
for (const value of result.values()) {
|
||||
if (value.status === 0) results.push(value.attachment);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
function getResultText() {
|
||||
const downloadedAttachmentsCount =
|
||||
attachments?.placeholders?.length - failedResults().length;
|
||||
if (downloadedAttachmentsCount === 0)
|
||||
return "Failed to download all attachments";
|
||||
return `Successfully downloaded ${downloadedAttachmentsCount}/${
|
||||
attachments?.placeholders.length
|
||||
} attachments as a zip file at ${
|
||||
Platform.OS === "android" ? "the selected folder" : "Notesnook/downloads"
|
||||
}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Heading>
|
||||
{downloading
|
||||
? "Downloading attachments"
|
||||
: result?.size
|
||||
? "Downloaded attachments"
|
||||
: "Download attachments"}
|
||||
</Heading>
|
||||
|
||||
{downloading ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{progress.statusText}
|
||||
</Paragraph>
|
||||
) : result?.size ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{getResultText()}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Are you sure you want to download all attachments
|
||||
{isNote ? " of this note?" : "?"}
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
{downloading ? (
|
||||
<View
|
||||
style={{
|
||||
width: 200,
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={
|
||||
progress.value
|
||||
? progress.value / attachments.placeholders?.length
|
||||
: 0
|
||||
}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<FlatList
|
||||
style={{
|
||||
maxHeight: 300,
|
||||
width: "100%",
|
||||
minHeight: 60,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 5,
|
||||
marginVertical: 12
|
||||
}}
|
||||
data={downloading ? attachments.placeholders : undefined}
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 60
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
No downloads in progress.
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
keyExtractor={(index) => "attachment_download" + index}
|
||||
renderItem={({ index }) => {
|
||||
return (
|
||||
<AttachmentItem
|
||||
id={index}
|
||||
setAttachments={() => {}}
|
||||
pressable={false}
|
||||
hideWhenNotDownloading={true}
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{result?.size ? (
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
type="accent"
|
||||
title="Done"
|
||||
/>
|
||||
) : !downloading ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
marginTop: 20
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 100,
|
||||
marginRight: 5
|
||||
}}
|
||||
onPress={() => {
|
||||
close?.();
|
||||
}}
|
||||
type="secondary"
|
||||
title="No"
|
||||
/>
|
||||
<Button
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 100,
|
||||
marginLeft: 5
|
||||
}}
|
||||
onPress={onDownload}
|
||||
type="accent"
|
||||
title="Yes"
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Button
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100,
|
||||
marginTop: 20
|
||||
}}
|
||||
onPress={cancel}
|
||||
type="error"
|
||||
title="Cancel"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
DownloadAttachments.present = (
|
||||
context: string,
|
||||
attachments: VirtualizedGrouping<Attachment>,
|
||||
isNote?: boolean
|
||||
) => {
|
||||
presentSheet({
|
||||
context: context,
|
||||
component: (ref, close, update) => (
|
||||
<DownloadAttachments
|
||||
close={close}
|
||||
attachments={attachments}
|
||||
isNote={isNote}
|
||||
update={update}
|
||||
/>
|
||||
)
|
||||
});
|
||||
};
|
||||
|
||||
export default DownloadAttachments;
|
||||
@@ -19,21 +19,28 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import {
|
||||
Attachment,
|
||||
FilteredSelector,
|
||||
Note,
|
||||
SortOptions,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
|
||||
import { ScrollView } from "react-native-gesture-handler";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import create, { State } from "zustand";
|
||||
import { db } from "../../common/database";
|
||||
import filesystem from "../../common/filesystem";
|
||||
import { presentSheet } from "../../services/event-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { downloadAttachments } from "../../common/filesystem/download-attachment";
|
||||
import { AttachmentGroupProgress } from "../../screens/settings/attachment-group-progress";
|
||||
import { presentSheet, ToastManager } from "../../services/event-manager";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Dialog } from "../dialog";
|
||||
import { presentDialog } from "../dialog/functions";
|
||||
import { Header } from "../header";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
@@ -42,22 +49,105 @@ import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { AttachmentItem } from "./attachment-item";
|
||||
import DownloadAttachments from "./download-attachments";
|
||||
|
||||
const DEFAULT_SORTING: SortOptions = {
|
||||
sortBy: "dateEdited",
|
||||
sortDirection: "desc"
|
||||
};
|
||||
|
||||
export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
type RecheckerProgress = {
|
||||
failed: number;
|
||||
passed: number;
|
||||
isWorking: boolean;
|
||||
shown: boolean;
|
||||
filter: string;
|
||||
};
|
||||
|
||||
interface RecheckerState extends State {
|
||||
progress: {
|
||||
[key: string]: RecheckerProgress;
|
||||
};
|
||||
setProgress: (key: string, progress: RecheckerProgress) => void;
|
||||
currentFilter: string;
|
||||
}
|
||||
|
||||
const useRechecker = create<RecheckerState>((set) => ({
|
||||
progress: {
|
||||
all: {
|
||||
failed: 0,
|
||||
passed: 0,
|
||||
isWorking: false,
|
||||
shown: false,
|
||||
filter: "all"
|
||||
}
|
||||
},
|
||||
currentFilter: "all",
|
||||
setProgress: (key: string, progress: RecheckerProgress) => {
|
||||
set((state) => ({
|
||||
progress: {
|
||||
...state.progress,
|
||||
[key]: progress
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
const attachmentTypes = [
|
||||
{
|
||||
title: "All",
|
||||
filterBy: "all"
|
||||
},
|
||||
{
|
||||
title: "Images",
|
||||
filterBy: "images"
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
filterBy: "documents"
|
||||
},
|
||||
{
|
||||
title: "Video",
|
||||
filterBy: "video"
|
||||
},
|
||||
{
|
||||
title: "Audio",
|
||||
filterBy: "audio"
|
||||
},
|
||||
{
|
||||
title: "Orphaned",
|
||||
filterBy: "orphaned"
|
||||
},
|
||||
{
|
||||
title: "Errors",
|
||||
filterBy: "errors"
|
||||
}
|
||||
];
|
||||
|
||||
export const AttachmentDialog = ({
|
||||
note,
|
||||
isSheet
|
||||
}: {
|
||||
note?: Note;
|
||||
isSheet: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { height } = useSettingStore((state) => state.dimensions);
|
||||
const [attachments, setAttachments] =
|
||||
useState<VirtualizedGrouping<Attachment>>();
|
||||
const attachmentSearchValue = useRef<string>();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const searchTimer = useRef<NodeJS.Timeout>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentFilter, setCurrentFilter] = useState("all");
|
||||
const rechecker = useRechecker((state) =>
|
||||
note ? state.progress[note.id] || {} : state.progress.all
|
||||
);
|
||||
const currentFilterRef = useRef(currentFilter);
|
||||
currentFilterRef.current = currentFilter;
|
||||
|
||||
useEffect(() => {
|
||||
useRechecker.setState({
|
||||
currentFilter: currentFilter
|
||||
});
|
||||
}, [currentFilter]);
|
||||
|
||||
const refresh = React.useCallback(() => {
|
||||
if (note) {
|
||||
@@ -68,6 +158,7 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => {
|
||||
setLoading(false);
|
||||
setAttachments(attachments);
|
||||
});
|
||||
} else {
|
||||
@@ -76,7 +167,10 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
...DEFAULT_SORTING,
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => setAttachments(attachments));
|
||||
.then((attachments) => {
|
||||
setAttachments(attachments);
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [note]);
|
||||
|
||||
@@ -108,52 +202,70 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
setAttachments={async () => {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
}}
|
||||
errorOnly={currentFilter === "errors"}
|
||||
attachments={attachments}
|
||||
id={index}
|
||||
context="attachments-list"
|
||||
context="global"
|
||||
/>
|
||||
);
|
||||
|
||||
const onCheck = async () => {
|
||||
if (!attachments) return;
|
||||
setLoading(true);
|
||||
const getState = () =>
|
||||
useRechecker.getState().progress[note?.id || "all"] || {};
|
||||
const setState = (state: Partial<RecheckerProgress>) =>
|
||||
useRechecker.getState().setProgress(note?.id || "all", {
|
||||
...getState(),
|
||||
...state
|
||||
});
|
||||
|
||||
for (let i = 0; i < attachments.placeholders.length; i++) {
|
||||
if (!attachments || getState().isWorking) return;
|
||||
setState({
|
||||
isWorking: true,
|
||||
failed: 0,
|
||||
passed: 0,
|
||||
shown: true,
|
||||
filter: currentFilterRef.current
|
||||
});
|
||||
const filter = currentFilterRef.current;
|
||||
const filteredAttachments = await filterAttachments(
|
||||
currentFilterRef.current
|
||||
);
|
||||
|
||||
for (let i = 0; i < filteredAttachments.placeholders.length; i++) {
|
||||
if (!getState().isWorking) {
|
||||
ToastManager.show({
|
||||
message: "Attachment recheck cancelled",
|
||||
type: "info",
|
||||
context: isSheet ? "local" : "global"
|
||||
});
|
||||
return;
|
||||
}
|
||||
const attachment = (await attachments.item(i))?.item;
|
||||
if (currentFilter == "errors" && !attachment?.failed) continue;
|
||||
|
||||
if (!attachment) continue;
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
if (result.failed) {
|
||||
setState({
|
||||
failed: getState().failed + 1
|
||||
});
|
||||
await db.attachments.markAsFailed(attachment.hash, result.failed);
|
||||
} else {
|
||||
await db.attachments.markAsFailed(attachment.id, undefined);
|
||||
setState({
|
||||
passed: getState().passed + 1
|
||||
});
|
||||
await db.attachments.markAsFailed(attachment.id);
|
||||
}
|
||||
}
|
||||
refresh();
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const attachmentTypes = [
|
||||
{
|
||||
title: "All",
|
||||
filterBy: "all"
|
||||
},
|
||||
{
|
||||
title: "Images",
|
||||
filterBy: "images"
|
||||
},
|
||||
{
|
||||
title: "Documents",
|
||||
filterBy: "documents"
|
||||
},
|
||||
{
|
||||
title: "Video",
|
||||
filterBy: "video"
|
||||
},
|
||||
{
|
||||
title: "Audio",
|
||||
filterBy: "audio"
|
||||
if (filter === useRechecker.getState().currentFilter) {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
}
|
||||
];
|
||||
|
||||
setState({
|
||||
isWorking: false
|
||||
});
|
||||
};
|
||||
|
||||
const filterAttachments = async (type: string) => {
|
||||
let items: FilteredSelector<Attachment> = db.attachments.all;
|
||||
@@ -183,6 +295,14 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
items = note
|
||||
? db.attachments.ofNote(note.id, "documents")
|
||||
: db.attachments.documents;
|
||||
break;
|
||||
case "orphaned":
|
||||
items = db.attachments.orphaned;
|
||||
break;
|
||||
case "errors":
|
||||
items = items = note
|
||||
? db.attachments.ofNote(note.id, "all")
|
||||
: db.attachments.all;
|
||||
}
|
||||
|
||||
return await items.sorted({
|
||||
@@ -192,39 +312,59 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
height: height * 0.85
|
||||
}}
|
||||
>
|
||||
<SheetProvider context="attachments-list" />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading>Attachments</Heading>
|
||||
|
||||
<>
|
||||
{isSheet ? (
|
||||
<>
|
||||
<SheetProvider context="attachments-list" />
|
||||
<Dialog context="local" />
|
||||
</>
|
||||
) : null}
|
||||
{!isSheet ? (
|
||||
<Header
|
||||
title="Manage attachments"
|
||||
renderedInRoute="SettingsGroup"
|
||||
canGoBack
|
||||
headerRightButtons={[
|
||||
{
|
||||
onPress() {
|
||||
onCheck();
|
||||
},
|
||||
title: "Recheck all"
|
||||
},
|
||||
{
|
||||
onPress() {
|
||||
if (!attachments) return;
|
||||
presentDialog({
|
||||
title: `Download ${attachments.placeholders.length} attachments`,
|
||||
paragraph:
|
||||
"Are you sure you want to download all attachments?",
|
||||
positiveText: "Download",
|
||||
positivePress: async () => {
|
||||
downloadAttachments(await attachments.ids());
|
||||
},
|
||||
negativeText: "Cancel"
|
||||
});
|
||||
},
|
||||
title: "Download all"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
) : (
|
||||
<Heading>Attachments</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="check-all"
|
||||
style={{
|
||||
@@ -236,125 +376,243 @@ export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
size={SIZE.lg}
|
||||
onPress={onCheck}
|
||||
/>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
name="download"
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
onPress={() => {
|
||||
if (!attachments) return;
|
||||
DownloadAttachments.present(
|
||||
"attachments-list",
|
||||
attachments,
|
||||
!!note
|
||||
);
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Seperator />
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
}}
|
||||
/>
|
||||
|
||||
<View>
|
||||
<ScrollView
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 50,
|
||||
flexDirection: "row",
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minWidth: "100%",
|
||||
height: 50
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{attachmentTypes.map((item) => (
|
||||
<Button
|
||||
type={
|
||||
currentFilter === item.filterBy ? "secondaryAccented" : "plain"
|
||||
}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
<IconButton
|
||||
name="download"
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
flexGrow: 1,
|
||||
borderBottomColor:
|
||||
currentFilter !== item.filterBy
|
||||
? "transparent"
|
||||
: colors.primary.accent
|
||||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
onPress={async () => {
|
||||
setCurrentFilter(item.filterBy);
|
||||
setAttachments(await filterAttachments(item.filterBy));
|
||||
color={colors.primary.paragraph}
|
||||
onPress={() => {
|
||||
if (!attachments) return;
|
||||
presentDialog({
|
||||
title: `Download ${attachments.placeholders.length} attachments`,
|
||||
paragraph:
|
||||
"Are you sure you want to download all attachments?",
|
||||
context: "local",
|
||||
positiveText: "Download",
|
||||
positivePress: async () => {
|
||||
downloadAttachments(await attachments.ids());
|
||||
},
|
||||
negativeText: "Cancel"
|
||||
});
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
<FlashList
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 150,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Icon name="attachment" size={60} color={colors.secondary.icon} />
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={attachments?.placeholders}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
<View
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 10
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="shield-key-outline"
|
||||
size={SIZE.xs}
|
||||
color={colors.primary.icon}
|
||||
<Seperator />
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
}}
|
||||
/>
|
||||
{" "}All attachments are end-to-end encrypted.
|
||||
</Paragraph>
|
||||
|
||||
{rechecker.shown ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
borderRadius: 10,
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
gap: 12,
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: 12,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{rechecker.isWorking ? (
|
||||
<ActivityIndicator color={colors.primary.accent} />
|
||||
) : (
|
||||
<Icon
|
||||
name="file-check"
|
||||
size={30}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
)}
|
||||
|
||||
<View>
|
||||
<Paragraph>
|
||||
{rechecker.isWorking
|
||||
? note
|
||||
? `Checking ${rechecker.filter.toLowerCase()} note attachments`
|
||||
: `Checking ${rechecker.filter.toLowerCase()} attachments`
|
||||
: "Attachments recheck complete"}
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
{`${rechecker.isWorking ? "Please wait... " : ""}Passed: ${
|
||||
rechecker.passed
|
||||
}, Failed: ${rechecker.failed}`}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<IconButton
|
||||
type={rechecker.isWorking ? "errorShade" : "plain"}
|
||||
name={rechecker.isWorking ? "close" : "check"}
|
||||
size={SIZE.lg}
|
||||
color={
|
||||
rechecker.isWorking ? colors.error.icon : colors.primary.accent
|
||||
}
|
||||
onPress={() => {
|
||||
useRechecker.getState().setProgress(note?.id || "all", {
|
||||
...useRechecker.getState().progress[note?.id || "all"],
|
||||
shown: false,
|
||||
isWorking: false
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View>
|
||||
<ScrollView
|
||||
style={{
|
||||
backgroundColor: colors.primary.background,
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
paddingVertical: 12
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
alignItems: "center"
|
||||
}}
|
||||
horizontal
|
||||
>
|
||||
{attachmentTypes.map((item) =>
|
||||
item.filterBy === "orphaned" && note ? null : (
|
||||
<Button
|
||||
type={
|
||||
currentFilter === item.filterBy
|
||||
? "secondaryAccented"
|
||||
: "plain"
|
||||
}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
fontSize={SIZE.sm}
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 12,
|
||||
height: 40,
|
||||
minWidth: 80
|
||||
}}
|
||||
onPress={async () => {
|
||||
const filterBy = item.filterBy;
|
||||
setCurrentFilter(filterBy);
|
||||
setLoading(true);
|
||||
filterAttachments(filterBy)
|
||||
.then((results) => {
|
||||
if (filterBy !== currentFilterRef.current) return;
|
||||
setAttachments(results);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(console.log);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
<FlashList
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
ListEmptyComponent={
|
||||
<View
|
||||
style={{
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator size={40} color={colors.primary.accent} />
|
||||
) : (
|
||||
<>
|
||||
<Icon
|
||||
name="attachment"
|
||||
size={60}
|
||||
color={colors.secondary.icon}
|
||||
/>
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
}
|
||||
ListHeaderComponent={<AllProgress note={note} />}
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={loading ? [] : attachments?.placeholders}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const AllProgress = ({ note }: { note?: Note }) => {
|
||||
const progress = useAttachmentStore((state) =>
|
||||
note
|
||||
? {
|
||||
downloading: {
|
||||
[note.id]: state.downloading?.[note.id]
|
||||
},
|
||||
uploading: {
|
||||
[note.id]: state.uploading?.[note.id]
|
||||
}
|
||||
}
|
||||
: state
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
gap: 10,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
{Object.keys(progress.downloading || {}).map((groupId) => (
|
||||
<AttachmentGroupProgress key={groupId} groupId={groupId} />
|
||||
))}
|
||||
|
||||
{Object.keys(progress.uploading || {}).map((groupId) => (
|
||||
<AttachmentGroupProgress key={groupId} groupId={groupId} />
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
AttachmentDialog.present = (note?: Note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog note={note} />
|
||||
component: () => <AttachmentDialog note={note} isSheet={true} />,
|
||||
keyboardHandlerDisabled: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -49,7 +44,6 @@ const AuthModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
|
||||
const actionSheetRef = useRef();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenLoginDialog, open);
|
||||
@@ -97,81 +91,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: 12,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent:
|
||||
initialAuthMode.current !== AuthMode.welcomeSignup
|
||||
? "space-between"
|
||||
: "flex-end"
|
||||
}}
|
||||
>
|
||||
{initialAuthMode.current === AuthMode.welcomeSignup ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{initialAuthMode.current !== AuthMode.welcomeSignup ? null : (
|
||||
<Button
|
||||
title="Skip"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
|
||||
@@ -68,7 +68,8 @@ export const ChangePassword = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(false, "change-password-dialog");
|
||||
if (!result) throw new Error("Failed to create backup");
|
||||
if (result.error)
|
||||
throw new Error(`Failed to create backup: ${result.error}`);
|
||||
|
||||
await db.user.clearSessions();
|
||||
await db.user.changePassword(oldPassword.current, password.current);
|
||||
|
||||
80
apps/mobile/app/components/auth/header.tsx
Normal file
80
apps/mobile/app/components/auth/header.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth } from "./common";
|
||||
|
||||
export const AuthHeader = (props: { welcome?: boolean }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
width: "100%",
|
||||
height: 50,
|
||||
justifyContent: !props.welcome ? "space-between" : "flex-end"
|
||||
}}
|
||||
>
|
||||
{props.welcome ? null : (
|
||||
<IconButton
|
||||
name="arrow-left"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!props.welcome ? null : (
|
||||
<Button
|
||||
title="Skip"
|
||||
onPress={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
iconSize={16}
|
||||
type="plain"
|
||||
iconPosition="right"
|
||||
icon="chevron-right"
|
||||
height={25}
|
||||
iconStyle={{
|
||||
marginTop: 2
|
||||
}}
|
||||
style={{
|
||||
paddingHorizontal: 6
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -17,12 +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 { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Sync from "../../services/sync";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
@@ -34,11 +39,6 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { ForgotPassword } from "./forgot-password";
|
||||
import { useLogin } from "./use-login";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eUserLoggedIn } from "../../utils/events";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import Sync from "../../services/sync";
|
||||
import { Notice } from "../ui/notice";
|
||||
|
||||
const LoginSteps = {
|
||||
emailAuth: 1,
|
||||
@@ -90,215 +90,233 @@ export const Login = ({ changeMode }) => {
|
||||
<>
|
||||
<ForgotPassword />
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<View
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: 12,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
extraBold
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Login to your {"\n"}account
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderBottomWidth: 0.8,
|
||||
marginBottom: 12,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor:
|
||||
deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Enter your email"
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
<Button
|
||||
title="Forgot your password?"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
height: 30,
|
||||
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={SIZE.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 10
|
||||
}}
|
||||
extraBold
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Login to your {"\n"}account
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25
|
||||
width: DDS.isTab
|
||||
? focused
|
||||
? "50%"
|
||||
: "49.99%"
|
||||
: focused
|
||||
? "100%"
|
||||
: "99.9%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Enter your email"
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<Button
|
||||
title="Cancel logging in"
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
height: 30,
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="errorShade"
|
||||
/>
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
<Button
|
||||
title="Forgot your password?"
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
height: 30,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
SheetManager.show("forgotpassword_sheet", email.current);
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
login();
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<Button
|
||||
title="Cancel logging in"
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
height: 30,
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
}}
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
fontSize={SIZE.xs}
|
||||
type="errorShade"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
Don't have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
Sign up
|
||||
Don't have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
Sign up
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,16 +16,12 @@ GNU General Public License for more details.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import Config from "./config";
|
||||
export const SignupContext = React.createContext<{
|
||||
signup?: () => Promise<boolean>;
|
||||
}>({
|
||||
signup: undefined
|
||||
});
|
||||
|
||||
export function isTelemetryEnabled() {
|
||||
// telemetry is always disabled in DEBUG/TEST mode
|
||||
if (import.meta.env.DEV || IS_TESTING) return false;
|
||||
|
||||
return Config.get("telemetry", false);
|
||||
}
|
||||
|
||||
export function setTelemetry(state: boolean) {
|
||||
Config.set("telemetry", state);
|
||||
}
|
||||
export const useSignupContext = () => useContext(SignupContext);
|
||||
@@ -17,26 +17,38 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { TouchableOpacity, View, useWindowDimensions } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { db } from "../../common/database";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage, setEmailVerifyMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Loading } from "../loading";
|
||||
import { PaywallComponent } from "../premium/component";
|
||||
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 { useSettingStore } from "../../stores/use-setting-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { AuthHeader } from "./header";
|
||||
import { SignupContext } from "./signup-context";
|
||||
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
selectPlan: 1,
|
||||
createAccount: 2
|
||||
};
|
||||
|
||||
export const Signup = ({ changeMode, welcome }) => {
|
||||
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
|
||||
const { colors } = useThemeColors();
|
||||
const email = useRef();
|
||||
const emailInputRef = useRef();
|
||||
@@ -49,7 +61,7 @@ export const Signup = ({ changeMode, trial }) => {
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const { height } = useWindowDimensions();
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastManager.show({
|
||||
@@ -68,22 +80,20 @@ 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();
|
||||
await sleep(300);
|
||||
if (trial) {
|
||||
PremiumService.sheet(null, null, true);
|
||||
} else {
|
||||
PremiumService.showVerifyEmailDialog();
|
||||
}
|
||||
setCurrentStep(SignupSteps.selectPlan);
|
||||
return true;
|
||||
} catch (e) {
|
||||
setCurrentStep(SignupSteps.signup);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "Signup failed",
|
||||
@@ -91,215 +101,266 @@ 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%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.4
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<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: 10
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Create your {"\n"}account
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center"
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Email"
|
||||
onSubmit={() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
validationType="password"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
onSubmit={() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Signup"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder="Confirm password"
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
By signing up, you agree to our{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Terms of Service{" "}
|
||||
</Paragraph>
|
||||
and{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Privacy Policy.
|
||||
</Paragraph>{" "}
|
||||
You also agree to recieve marketing emails from us which you can
|
||||
opt-out of from app settings.
|
||||
</Paragraph>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={signup}
|
||||
fontSize={SIZE.md}
|
||||
style={{
|
||||
marginRight: 12,
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.xs + 1} color={colors.secondary.paragraph}>
|
||||
Already have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 0.8,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor:
|
||||
deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
width: deviceMode === "mobile" ? null : "50%",
|
||||
minHeight: height * 0.25
|
||||
}}
|
||||
>
|
||||
Login
|
||||
<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={SIZE.xxl}
|
||||
>
|
||||
Create your account
|
||||
</Heading>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: DDS.isTab ? "50%" : "100%",
|
||||
paddingHorizontal: 16,
|
||||
backgroundColor: colors.primary.background,
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
fwdRef={emailInputRef}
|
||||
onChangeText={(value) => {
|
||||
email.current = value;
|
||||
}}
|
||||
testID="input.email"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
autoComplete="email"
|
||||
validationType="email"
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
errorMessage="Email is invalid"
|
||||
placeholder="Email"
|
||||
blurOnSubmit={false}
|
||||
onSubmit={() => {
|
||||
if (!email.current) return;
|
||||
passwordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Next"
|
||||
returnKeyType="next"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
blurOnSubmit={false}
|
||||
validationType="password"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
onSubmit={() => {
|
||||
if (!password.current) return;
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
fwdRef={confirmPasswordInputRef}
|
||||
onChangeText={(value) => {
|
||||
confirmPassword.current = value;
|
||||
}}
|
||||
testID="input.confirmPassword"
|
||||
onErrorCheck={(e) => setError(e)}
|
||||
returnKeyLabel="Signup"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
blurOnSubmit={false}
|
||||
validationType="confirmPassword"
|
||||
customValidator={() => password.current}
|
||||
placeholder="Confirm password"
|
||||
marginBottom={12}
|
||||
onSubmit={signup}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={!loading ? "Continue" : null}
|
||||
type="accent"
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
signup();
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
width="100%"
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(0);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
Already have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
Login
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
paddingHorizontal: 16,
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
By signing up, you agree to our{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Terms of Service{" "}
|
||||
</Paragraph>
|
||||
and{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy", colors);
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Privacy Policy.
|
||||
</Paragraph>{" "}
|
||||
You also agree to recieve marketing emails from us which you can
|
||||
opt-out of from app settings.
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</>
|
||||
) : currentStep === SignupSteps.createAccount ? (
|
||||
<>
|
||||
<Loading
|
||||
title="Setting up your account..."
|
||||
description="Your account is almost ready, please wait..."
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PaywallComponent
|
||||
close={() => {
|
||||
hideAuth();
|
||||
}}
|
||||
setupAccount={() => {
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
}}
|
||||
isModal={false}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SignupContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,6 +37,7 @@ import Seperator from "../ui/seperator";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { useCallback } from "react";
|
||||
import { ScrollView } from "react-native-actions-sheet";
|
||||
|
||||
const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -143,7 +144,10 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
}, [currentMethod.method, mfaInfo.token, seconds, sending, start]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ScrollView
|
||||
keyboardShouldPersistTaps="handled"
|
||||
keyboardDismissMode="interactive"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignItems: "center",
|
||||
@@ -214,6 +218,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
code.current = value;
|
||||
//onNext();
|
||||
}}
|
||||
onSubmitEditing={onNext}
|
||||
caretHidden
|
||||
inputStyle={{
|
||||
fontSize: SIZE.lg,
|
||||
@@ -225,6 +230,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
keyboardType={
|
||||
currentMethod.method === "recoveryCode" ? "default" : "numeric"
|
||||
}
|
||||
enablesReturnKeyAutomatically
|
||||
containerStyle={{
|
||||
height: 60,
|
||||
borderWidth: 0,
|
||||
@@ -297,7 +303,7 @@ const TwoFactorVerification = ({ onMfaLogin, mfaInfo }) => {
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,8 +18,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { TextInput } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
import { 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";
|
||||
@@ -33,15 +34,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 (
|
||||
@@ -68,44 +72,57 @@ 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);
|
||||
console.log("email auth", mfaInfo);
|
||||
if (mfaInfo) {
|
||||
TwoFactorVerification.present(async (mfa, callback) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
TwoFactorVerification.present(
|
||||
async (
|
||||
mfa: { code: string; method: string },
|
||||
callback: (value: boolean) => void
|
||||
) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
|
||||
if (success) {
|
||||
setStep(LoginSteps.passwordAuth);
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 1);
|
||||
callback && callback(true);
|
||||
if (success) {
|
||||
setStep(LoginSteps.passwordAuth);
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 1);
|
||||
callback && callback(true);
|
||||
}
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if ((e as Error).message === "invalid_grant") {
|
||||
eSendEvent(eCloseSheet, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
}
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if (e.message === "invalid_grant") {
|
||||
eSendEvent(eCloseSheet, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
}
|
||||
}, mfaInfo);
|
||||
},
|
||||
mfaInfo
|
||||
);
|
||||
} else {
|
||||
finishWithError(new Error("Unable to send 2FA code"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case LoginSteps.passwordAuth: {
|
||||
if (!email.current || !password.current) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
await db.user.authenticatePassword(
|
||||
email.current,
|
||||
password.current,
|
||||
null,
|
||||
undefined,
|
||||
sessionExpired
|
||||
);
|
||||
finishLogin();
|
||||
@@ -114,11 +131,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({
|
||||
@@ -23,6 +23,7 @@ import { AnnouncementDialog } from "../announcements";
|
||||
import AuthModal from "../auth/auth-modal";
|
||||
import { SessionExpired } from "../auth/session-expired";
|
||||
import { Dialog } from "../dialog";
|
||||
import { AppLockPassword } from "../dialogs/applock-password";
|
||||
import JumpToSectionDialog from "../dialogs/jump-to-section";
|
||||
import { LoadingDialog } from "../dialogs/loading";
|
||||
import PDFPreview from "../dialogs/pdf-preview";
|
||||
@@ -35,8 +36,7 @@ import { Expiring } from "../premium/expiring";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
import { AppLockPassword } from "../dialogs/applock-password";
|
||||
import Progress from "../dialogs/progress";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -52,7 +52,6 @@ const DialogProvider = () => {
|
||||
<RecoveryKeySheet colors={colors} />
|
||||
<SheetProvider />
|
||||
<SheetProvider context="sync_progress" />
|
||||
<RestoreDataSheet />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
@@ -62,6 +61,7 @@ const DialogProvider = () => {
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
<Progress />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,21 +32,29 @@ const DialogContainer = ({
|
||||
}: ViewProps & {
|
||||
width?: any;
|
||||
height?: any;
|
||||
noBorder?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...restProps}
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
width: width || DDS.isTab ? 500 : "85%",
|
||||
maxHeight: height || 450,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12,
|
||||
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
|
||||
}}
|
||||
style={[
|
||||
style,
|
||||
{
|
||||
width: width || DDS.isTab ? 500 : "85%",
|
||||
maxHeight: height || 450,
|
||||
borderRadius: 10,
|
||||
backgroundColor: colors.primary.background,
|
||||
paddingTop: 12
|
||||
},
|
||||
restProps?.noBorder
|
||||
? {}
|
||||
: {
|
||||
...getElevationStyle(5),
|
||||
...getContainerBorder(colors.secondary.background, 0.8, 0.05)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ type DialogInfo = {
|
||||
paragraph?: string;
|
||||
positiveText?: string;
|
||||
negativeText?: string;
|
||||
positivePress?: (value: any) => void;
|
||||
positivePress?: (...args: any[]) => void;
|
||||
onClose?: () => void;
|
||||
positiveType?:
|
||||
| "transparent"
|
||||
@@ -45,6 +45,15 @@ type DialogInfo = {
|
||||
context: "global" | "local" | (string & {});
|
||||
secureTextEntry?: boolean;
|
||||
keyboardType?: string;
|
||||
check?: {
|
||||
info: string;
|
||||
type?: string;
|
||||
defaultValue?: boolean;
|
||||
};
|
||||
notice: {
|
||||
text: string;
|
||||
type: "alert" | "information";
|
||||
};
|
||||
};
|
||||
|
||||
export function presentDialog(data: Partial<DialogInfo>): void {
|
||||
|
||||
@@ -37,6 +37,7 @@ import DialogHeader from "./dialog-header";
|
||||
import { useCallback } from "react";
|
||||
import { Button } from "../ui/button";
|
||||
import { getContainerBorder } from "../../utils/colors";
|
||||
import { Notice } from "../ui/notice";
|
||||
|
||||
export const Dialog = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -62,7 +63,8 @@ export const Dialog = ({ context = "global" }) => {
|
||||
disableBackdropClosing: false,
|
||||
check: {
|
||||
info: "Check",
|
||||
type: "transparent"
|
||||
type: "transparent",
|
||||
defaultValue: false
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,7 +88,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (!data.context) data.context = "global";
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setChecked(false);
|
||||
setChecked(data.check?.defaultValue);
|
||||
values.current.inputValue = data.defaultValue;
|
||||
setVisible(true);
|
||||
},
|
||||
@@ -180,6 +182,19 @@ export const Dialog = ({ context = "global" }) => {
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo?.notice ? (
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<Notice
|
||||
type={dialogInfo.notice.type || "information"}
|
||||
text={dialogInfo.notice.text}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{dialogInfo.check ? (
|
||||
<>
|
||||
<Button
|
||||
@@ -198,7 +213,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={checked ? dialogInfo.check.type || "selected" : "plain"}
|
||||
type={checked ? dialogInfo.check.type || "plain" : "plain"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -44,6 +44,7 @@ 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";
|
||||
|
||||
const WIN_WIDTH = Dimensions.get("window").width;
|
||||
const WIN_HEIGHT = Dimensions.get("window").height;
|
||||
@@ -124,6 +125,7 @@ const PDFPreview = () => {
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
deleteCacheFileByPath(pdfSource);
|
||||
setPDFSource(null);
|
||||
setVisible(false);
|
||||
setPassword("");
|
||||
|
||||
198
apps/mobile/app/components/dialogs/progress/index.tsx
Normal file
198
apps/mobile/app/components/dialogs/progress/index.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
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, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { eSendEvent, eSubscribeEvent } from "../../../services/event-manager";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import BaseDialog from "../../dialog/base-dialog";
|
||||
import DialogContainer from "../../dialog/dialog-container";
|
||||
import { Button } from "../../ui/button";
|
||||
import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
export type ProgressOptions = {
|
||||
progress?: string;
|
||||
cancelCallback?: () => void;
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
fillBackground?: boolean;
|
||||
canHideProgress?: boolean;
|
||||
};
|
||||
|
||||
export const PROGRESS_EVENTS = {
|
||||
start: "startProgress",
|
||||
end: "endProgress",
|
||||
update: "updateProgress"
|
||||
};
|
||||
|
||||
export default function Progress() {
|
||||
const { colors } = useThemeColors();
|
||||
const [progress, setProgress] = React.useState<string | undefined>();
|
||||
const [visible, setVisible] = React.useState(false);
|
||||
const cancelCallback = React.useRef<() => void>();
|
||||
const [data, setData] = React.useState<{
|
||||
title?: string;
|
||||
paragraph?: string;
|
||||
fillBackground?: boolean;
|
||||
canHideProgress?: boolean;
|
||||
}>();
|
||||
|
||||
useEffect(() => {
|
||||
const events = [
|
||||
eSubscribeEvent(PROGRESS_EVENTS.start, (options: ProgressOptions) => {
|
||||
setProgress(options.progress);
|
||||
cancelCallback.current = options.cancelCallback;
|
||||
console.log("options", options.fillBackground);
|
||||
setData({
|
||||
title: options.title,
|
||||
paragraph: options.paragraph,
|
||||
fillBackground: options.fillBackground,
|
||||
canHideProgress: options.canHideProgress
|
||||
});
|
||||
setVisible(true);
|
||||
}),
|
||||
eSubscribeEvent(PROGRESS_EVENTS.end, () => {
|
||||
setProgress(undefined);
|
||||
setVisible(false);
|
||||
setData(undefined);
|
||||
cancelCallback.current?.();
|
||||
cancelCallback.current = undefined;
|
||||
}),
|
||||
eSubscribeEvent(PROGRESS_EVENTS.update, (options: ProgressOptions) => {
|
||||
setProgress(options.progress);
|
||||
if (options.cancelCallback) {
|
||||
cancelCallback.current = options.cancelCallback;
|
||||
}
|
||||
|
||||
const data: ProgressOptions = {};
|
||||
if (options.title) data.title = options.title;
|
||||
if (options.paragraph) data.paragraph = options.paragraph;
|
||||
if (options.fillBackground)
|
||||
data.fillBackground = options.fillBackground;
|
||||
|
||||
setData((current) => {
|
||||
return {
|
||||
...current,
|
||||
...data
|
||||
};
|
||||
});
|
||||
})
|
||||
];
|
||||
return () => {
|
||||
events.forEach((event) => event?.unsubscribe());
|
||||
cancelCallback.current = undefined;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return !visible ? null : (
|
||||
<BaseDialog
|
||||
background={data?.fillBackground ? colors.primary.background : undefined}
|
||||
visible
|
||||
>
|
||||
<DialogContainer
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingBottom: 10
|
||||
}}
|
||||
noBorder={data?.fillBackground ? true : false}
|
||||
>
|
||||
<Dialog context="local" />
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12,
|
||||
gap: 10,
|
||||
paddingBottom: 20
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
{data?.title}
|
||||
</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
{progress ? progress : data?.paragraph}
|
||||
</Paragraph>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{data?.canHideProgress ? null : (
|
||||
<Button
|
||||
title={cancelCallback.current ? "Cancel" : "Hide"}
|
||||
type="secondaryAccented"
|
||||
onPress={() => {
|
||||
if (cancelCallback.current) {
|
||||
cancelCallback.current?.();
|
||||
}
|
||||
setVisible(false);
|
||||
setProgress(undefined);
|
||||
setData(undefined);
|
||||
}}
|
||||
width="100%"
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</DialogContainer>
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function startProgress(options: ProgressOptions) {
|
||||
eSendEvent(PROGRESS_EVENTS.start, options);
|
||||
}
|
||||
|
||||
export function endProgress() {
|
||||
eSendEvent(PROGRESS_EVENTS.end);
|
||||
}
|
||||
|
||||
export function updateProgress(options: ProgressOptions) {
|
||||
eSendEvent(PROGRESS_EVENTS.update, options);
|
||||
}
|
||||
@@ -23,10 +23,7 @@ import { SafeAreaProvider, SafeAreaView } from "react-native-safe-area-context";
|
||||
import { Dialog } from "../dialog";
|
||||
import { Issue } from "../sheets/github/issue";
|
||||
|
||||
const error = (
|
||||
stack: string,
|
||||
component: string
|
||||
) => `Please let us know what happened. What steps we can take to reproduce the issue here.
|
||||
const error = (stack: string, component: string) => `
|
||||
|
||||
_______________________________
|
||||
Stacktrace: In ${component}::${stack}`;
|
||||
@@ -39,9 +36,9 @@ class ExceptionHandler extends React.Component<{
|
||||
error: Error | null;
|
||||
hasError: boolean;
|
||||
} = {
|
||||
hasError: false,
|
||||
error: null
|
||||
};
|
||||
hasError: false,
|
||||
error: null
|
||||
};
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error: error };
|
||||
}
|
||||
|
||||
@@ -67,8 +67,6 @@ export const Header = ({
|
||||
hasSearch?: boolean;
|
||||
onSearch?: () => void;
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [borderHidden, setBorderHidden] = useState(true);
|
||||
|
||||
@@ -96,7 +96,8 @@ export const RightMenus = ({
|
||||
animationDuration={200}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
backgroundColor: contextMenuColors.primary.background
|
||||
backgroundColor: contextMenuColors.primary.background,
|
||||
marginTop: -40
|
||||
}}
|
||||
onRequestClose={() => {
|
||||
menuRef.current?.hide();
|
||||
|
||||
@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import dataurl from "@notesnook/core/dist/utils/dataurl";
|
||||
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
|
||||
import { DataURL } from "@notesnook/core";
|
||||
import type { ImageAttributes } from "@notesnook/editor";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
@@ -55,8 +55,8 @@ const ImagePreview = () => {
|
||||
setLoading(true);
|
||||
setTimeout(async () => {
|
||||
let hash = image.hash;
|
||||
if (!hash && image.src && dataurl.toObject(image.src)) {
|
||||
const data = dataurl.toObject(image.src);
|
||||
if (!hash && image.src && DataURL.toObject(image.src)) {
|
||||
const data = DataURL.toObject(image.src);
|
||||
if (!data) return;
|
||||
hash = await Sodium.hashFile({
|
||||
data: data.data,
|
||||
|
||||
@@ -19,38 +19,51 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import {
|
||||
Linking,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
import { Linking, ScrollView, useWindowDimensions, View } from "react-native";
|
||||
import { SwiperFlatList } from "react-native-swiper-flatlist";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eOpenLoginDialog } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { AuthMode } from "../auth";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import Navigation from "../../services/navigation";
|
||||
import { eOpenLoginDialog } from "../../utils/events";
|
||||
import { AuthMode } from "../auth";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
|
||||
const Intro = ({ navigation }) => {
|
||||
type IntroItem = {
|
||||
headings?: string[];
|
||||
body?: string;
|
||||
testimonial?: string;
|
||||
link?: string;
|
||||
user?: string;
|
||||
};
|
||||
|
||||
const introItems: IntroItem[] = [
|
||||
{
|
||||
headings: ["Open source.", "End to end encrypted.", "Private."],
|
||||
body: "Write notes with freedom, no spying, no tracking."
|
||||
},
|
||||
{
|
||||
headings: ["Privacy for everyone", "— not just the", "privileged few"],
|
||||
body: "Your privacy matters to us, no matter who you are. In a world where everyone is trying to spy on you, Notesnook encrypts all your data before it leaves your device. With Notesnook no one can ever sell your data again."
|
||||
},
|
||||
{
|
||||
testimonial:
|
||||
"You simply cannot get any better of a note taking app than @notesnook. The UI is clean and slick, it is feature rich, encrypted, reasonably priced (esp. for students & educators) & open source",
|
||||
link: "https://twitter.com/andrewsayer/status/1637817220113002503",
|
||||
user: "@andrewsayer on Twitter"
|
||||
}
|
||||
];
|
||||
|
||||
const Intro = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const isTelemetryEnabled = useSettingStore(
|
||||
(state) => state.settings.telemetry
|
||||
);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const { width } = useWindowDimensions();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const renderItem = React.useCallback(
|
||||
({ item }) => (
|
||||
({ item }: { item: IntroItem }) => (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
@@ -105,17 +118,18 @@ const Intro = ({ navigation }) => {
|
||||
|
||||
{item.body ? <Paragraph size={SIZE.sm}>{item.body}</Paragraph> : null}
|
||||
|
||||
{item.tesimonial ? (
|
||||
{item.testimonial ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
fontSize: SIZE.lg
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!item.link) return;
|
||||
Linking.openURL(item.link);
|
||||
}}
|
||||
>
|
||||
{item.tesimonial} — {item.user}
|
||||
{item.testimonial} — {item.user}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -130,22 +144,23 @@ const Intro = ({ navigation }) => {
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: deviceMode !== "mobile" ? width / 2 : "100%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
marginBottom: 20,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.primary.border,
|
||||
alignSelf: deviceMode !== "mobile" ? "center" : undefined,
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : null,
|
||||
borderColor: deviceMode !== "mobile" ? colors.primary.border : null,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : null,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : null,
|
||||
paddingTop: insets.top + 10,
|
||||
paddingBottom: insets.top + 10,
|
||||
minHeight: height * 0.7
|
||||
borderWidth: deviceMode !== "mobile" ? 1 : undefined,
|
||||
borderColor:
|
||||
deviceMode !== "mobile" ? colors.primary.border : undefined,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : undefined,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : undefined,
|
||||
flexGrow: 2
|
||||
}}
|
||||
>
|
||||
<SwiperFlatList
|
||||
@@ -155,26 +170,7 @@ const Intro = ({ navigation }) => {
|
||||
index={0}
|
||||
useReactNativeGestureHandler={true}
|
||||
showPagination
|
||||
data={[
|
||||
{
|
||||
headings: ["Open source.", "End to end encrypted.", "Private."],
|
||||
body: "Write notes with freedom, no spying, no tracking."
|
||||
},
|
||||
{
|
||||
headings: [
|
||||
"Privacy for everyone",
|
||||
"— not just the",
|
||||
"privileged few"
|
||||
],
|
||||
body: "Your privacy matters to us, no matter who you are. In a world where everyone is trying to spy on you, Notesnook encrypts all your data before it leaves your device. With Notesnook no one can ever sell your data again."
|
||||
},
|
||||
{
|
||||
tesimonial:
|
||||
"You simply cannot get any better of a note taking app than @notesnook. The UI is clean and slick, it is feature rich, encrypted, reasonably priced (esp. for students & educators) & open source",
|
||||
link: "https://twitter.com/andrewsayer/status/1637817220113002503",
|
||||
user: "@andrewsayer on Twitter"
|
||||
}
|
||||
]}
|
||||
data={introItems}
|
||||
paginationActiveColor={colors.primary.accent}
|
||||
paginationStyleItem={{
|
||||
width: 10,
|
||||
@@ -190,13 +186,14 @@ const Intro = ({ navigation }) => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
minHeight: height * 0.3
|
||||
paddingHorizontal: 16,
|
||||
gap: 12,
|
||||
paddingVertical: 16
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
width={250}
|
||||
onPress={async () => {
|
||||
width="100%"
|
||||
onPress={() => {
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
|
||||
setTimeout(() => {
|
||||
SettingsService.set({
|
||||
@@ -210,51 +207,29 @@ const Intro = ({ navigation }) => {
|
||||
style={{
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "center",
|
||||
...getElevationStyle(5),
|
||||
borderRadius: 100
|
||||
...getElevationStyle(5)
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title="Get started"
|
||||
/>
|
||||
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
width: "90%",
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "center",
|
||||
padding: 12,
|
||||
maxWidth: 500
|
||||
}}
|
||||
<Button
|
||||
width="100%"
|
||||
title="I already have an account"
|
||||
type="secondary"
|
||||
onPress={() => {
|
||||
SettingsService.set({ telemetry: !isTelemetryEnabled });
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.login);
|
||||
setTimeout(() => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.replace("Notes", {
|
||||
canGoBack: false
|
||||
});
|
||||
}, 1000);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
size={SIZE.md}
|
||||
name={
|
||||
isTelemetryEnabled ? "checkbox-marked" : "checkbox-blank-outline"
|
||||
}
|
||||
color={
|
||||
isTelemetryEnabled ? colors.primary.accent : colors.primary.icon
|
||||
}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
marginLeft: 6
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
Help improve Notesnook by sending completely anonymized{" "}
|
||||
<Heading size={SIZE.xs}>private analytics and bug reports.</Heading>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
/>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
File diff suppressed because one or more lines are too long
@@ -49,7 +49,8 @@ export const Card = ({ color }: { color?: string }) => {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 0
|
||||
paddingHorizontal: 0,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
@@ -91,7 +92,6 @@ export const Card = ({ color }: { color?: string }) => {
|
||||
<View
|
||||
style={{
|
||||
marginLeft: 10,
|
||||
flexShrink: 1,
|
||||
marginRight: 10
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
TrashItem,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
|
||||
import { getSortValue } from "@notesnook/core";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,292 +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, { useState } from "react";
|
||||
import { ActivityIndicator, Platform, ScrollView, View } from "react-native";
|
||||
import { LAUNCH_ROCKET } from "../../assets/images/assets";
|
||||
import { db } from "../../common/database";
|
||||
import { usePricing } from "../../hooks/use-pricing";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import { eSendEvent, presentSheet } from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import {
|
||||
eClosePremiumDialog,
|
||||
eCloseSheet,
|
||||
eOpenLoginDialog
|
||||
} from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { AuthMode } from "../auth";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import { Toast } from "../toast";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import Seperator from "../ui/seperator";
|
||||
import { SvgView } from "../ui/svg";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import { features } from "./features";
|
||||
import { Group } from "./group";
|
||||
import { PricingPlans } from "./pricing-plans";
|
||||
|
||||
export const Component = ({ close, promo }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const user = useUserStore((state) => state.user);
|
||||
const userCanRequestTrial =
|
||||
user && (!user.subscription || !user.subscription.expiry) ? true : false;
|
||||
const [floatingButton, setFloatingButton] = useState(false);
|
||||
const pricing = usePricing("monthly");
|
||||
|
||||
const onPress = async () => {
|
||||
if (user) {
|
||||
presentSheet({
|
||||
context: "pricing_plans",
|
||||
component: (
|
||||
<PricingPlans showTrialOption={false} marginTop={1} promo={promo} />
|
||||
)
|
||||
});
|
||||
} else {
|
||||
close();
|
||||
setTimeout(() => {
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.trialSignup);
|
||||
}, 400);
|
||||
}
|
||||
};
|
||||
|
||||
const onScroll = (event) => {
|
||||
let contentSize = event.nativeEvent.contentSize.height;
|
||||
contentSize = contentSize - event.nativeEvent.layoutMeasurement.height;
|
||||
let yOffset = event.nativeEvent.contentOffset.y;
|
||||
if (yOffset > 600 && yOffset < contentSize - 400) {
|
||||
setFloatingButton(true);
|
||||
} else {
|
||||
setFloatingButton(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "space-between",
|
||||
borderRadius: 10,
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
>
|
||||
<SheetProvider context="pricing_plans" />
|
||||
<IconButton
|
||||
onPress={() => {
|
||||
close();
|
||||
}}
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: DDS.isTab ? 30 : 15,
|
||||
top: Platform.OS === "ios" ? 0 : 30,
|
||||
zIndex: 10,
|
||||
width: 50,
|
||||
height: 50
|
||||
}}
|
||||
color={colors.primary.paragraph}
|
||||
name="close"
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
style={{
|
||||
paddingHorizontal: DDS.isTab ? DDS.width / 5 : 0
|
||||
}}
|
||||
scrollEventThrottle={0}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
onScroll={onScroll}
|
||||
>
|
||||
<View
|
||||
key="top-banner"
|
||||
style={{
|
||||
width: "100%",
|
||||
alignItems: "center",
|
||||
height: 400,
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<SvgView
|
||||
width={350}
|
||||
height={350}
|
||||
src={LAUNCH_ROCKET(colors.primary.accent)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Heading
|
||||
key="heading"
|
||||
size={SIZE.lg}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
paddingTop: 20
|
||||
}}
|
||||
>
|
||||
Notesnook{" "}
|
||||
<Heading size={SIZE.lg} color={colors.primary.accent}>
|
||||
Pro
|
||||
</Heading>
|
||||
</Heading>
|
||||
|
||||
{!pricing ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={SIZE.md}
|
||||
color={colors.primary.accent}
|
||||
/>
|
||||
) : (
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
(
|
||||
{Platform.OS === "android"
|
||||
? pricing.product?.subscriptionOfferDetails[0].pricingPhases
|
||||
.pricingPhaseList?.[0].formattedPrice
|
||||
: pricing.product?.localizedPrice}{" "}
|
||||
/ mo)
|
||||
</Paragraph>
|
||||
)}
|
||||
|
||||
<Paragraph
|
||||
key="description"
|
||||
size={SIZE.md}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
textAlign: "center",
|
||||
alignSelf: "center",
|
||||
paddingBottom: 20,
|
||||
width: "90%"
|
||||
}}
|
||||
>
|
||||
Ready to take the next step on your private note taking journey?
|
||||
</Paragraph>
|
||||
|
||||
{userCanRequestTrial ? (
|
||||
<Button
|
||||
key="calltoaction"
|
||||
onPress={async () => {
|
||||
try {
|
||||
await db.user.activateTrial();
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Walkthrough.present("trialstarted", false, true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
title="Try free for 14 days"
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 15,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
key="calltoaction"
|
||||
onPress={onPress}
|
||||
title={
|
||||
promo ? promo.text : user ? "See all plans" : "Sign up for free"
|
||||
}
|
||||
type={userCanRequestTrial ? "secondaryAccented" : "accent"}
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 15,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
{!user || userCanRequestTrial ? (
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center",
|
||||
marginTop: 10,
|
||||
maxWidth: "80%"
|
||||
}}
|
||||
>
|
||||
{user
|
||||
? 'On clicking "Try free for 14 days", your free trial will be activated.'
|
||||
: "After sign up you will be asked to activate your free trial."}{" "}
|
||||
<Paragraph size={SIZE.xs} style={{ fontWeight: "bold" }}>
|
||||
No credit card is required.
|
||||
</Paragraph>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<Seperator key="seperator_1" />
|
||||
|
||||
{features.map((item, index) => (
|
||||
<Group key={item.title} item={item} index={index} />
|
||||
))}
|
||||
|
||||
<View
|
||||
key="plans"
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
<PricingPlans showTrialOption={false} promo={promo} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{floatingButton ? (
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={
|
||||
promo ? promo.text : user ? "See all plans" : "Sign up for free"
|
||||
}
|
||||
type="accent"
|
||||
style={{
|
||||
paddingHorizontal: 24,
|
||||
position: "absolute",
|
||||
borderRadius: 100,
|
||||
bottom: 30,
|
||||
...getElevationStyle(10)
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Toast context="local" />
|
||||
<View
|
||||
style={{
|
||||
paddingBottom: 10
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
768
apps/mobile/app/components/premium/component.tsx
Normal file
768
apps/mobile/app/components/premium/component.tsx
Normal file
File diff suppressed because one or more lines are too long
60
apps/mobile/app/components/premium/features-list.tsx
Normal file
60
apps/mobile/app/components/premium/features-list.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
export const FeaturesList = {
|
||||
// Monthly: ["$0/mo", "$1.99/mo", "$5.99/mo", "$9.99/mo"],
|
||||
// Yearly: ["$0/yr", "$14.99/yr", "$59.99/yr", "$109.99/yr"],
|
||||
// "5 year": [null, null, "$249.99/5yr", "$449.99/5yr"],
|
||||
Plans: ["Free", "Essential", "Pro", "Believer"],
|
||||
Devices: [2, 5, "Unlimited", "Unlimited"],
|
||||
Storage: ["100MB/mo", "1GB/mo", "10GB/mo", "25GB/mo"],
|
||||
"File size": ["10MB", "100MB", "1GB", "5GB"],
|
||||
"File compression": [true, true, "Optional", "Optional"],
|
||||
"Check list": [true, true, true, true],
|
||||
"Bullet list": [true, true, true, true],
|
||||
Quote: [true, true, true, true],
|
||||
"Note link": [true, true, true, true],
|
||||
"Task list": [false, true, true, true],
|
||||
"Outline list": [false, true, true, true],
|
||||
Callouts: [false, true, true, true],
|
||||
Colors: [7, 20, "∞", "∞"],
|
||||
Tags: [50, 500, "∞", "∞"],
|
||||
Notebooks: [100, 1000, "∞", "∞"],
|
||||
"Active Reminders": [10, 50, "∞", "∞"],
|
||||
Shortcuts: [10, "∞", "∞", "∞"],
|
||||
"Note history": ["Local", "Local", "Synced", "Synced"],
|
||||
"Security key app lock": [false, false, true, true],
|
||||
"Pin note in notification": [false, false, true, true],
|
||||
"Create note from notification drawer": [false, false, true, true],
|
||||
"Default startup screen": [false, false, true, true],
|
||||
"Markdown shortcuts": [false, true, true, true],
|
||||
"Custom toolbar preset": [false, false, true, true],
|
||||
"Customizable navigation menu": [false, true, true, true],
|
||||
"Files in monograph": [
|
||||
"Images",
|
||||
"Images & files",
|
||||
"Images & files",
|
||||
"Images & files"
|
||||
],
|
||||
"Note history retention": ["1 month", "∞", "∞", "∞"],
|
||||
"App lock": ["immediate", "immediate", "any", "any"],
|
||||
"Password on monograph": ["Default", "Custom", "Custom", "Custom"],
|
||||
"Voice memos": ["5 minute", "1 hour", "∞", true],
|
||||
"Notebook publishing": [1, 2, 5, true],
|
||||
Vaults: [1, 3, "∞", "∞"]
|
||||
};
|
||||
@@ -24,7 +24,8 @@ import {
|
||||
} from "../../services/event-manager";
|
||||
import { eClosePremiumDialog, eOpenPremiumDialog } from "../../utils/events";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Component } from "./component";
|
||||
import { PaywallComponent } from "./component";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
|
||||
class PremiumDialog extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -74,7 +75,7 @@ class PremiumDialog extends React.Component {
|
||||
background={this.props.colors.primary.background}
|
||||
onRequestClose={this.onClose}
|
||||
>
|
||||
<Component
|
||||
<PaywallComponent
|
||||
getRef={() => this.actionSheetRef}
|
||||
promo={this.state.promo}
|
||||
close={this.close}
|
||||
|
||||
@@ -85,7 +85,7 @@ export const PricingItem = ({
|
||||
>
|
||||
{Platform.OS === "android"
|
||||
? (product.data as RNIap.SubscriptionAndroid | undefined)
|
||||
?.subscriptionOfferDetails[0].pricingPhases
|
||||
?.subscriptionOfferDetails?.[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (product.data as RNIap.SubscriptionIOS | undefined)
|
||||
?.localizedPrice}
|
||||
|
||||
@@ -48,6 +48,7 @@ import Paragraph from "../ui/typography/paragraph";
|
||||
import { Walkthrough } from "../walkthroughs";
|
||||
import { PricingItem } from "./pricing-item";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import usePricingPlans from "../../hooks/use-pricing-plans";
|
||||
|
||||
const UUID_PREFIX = "0bdaea";
|
||||
const UUID_VERSION = "4";
|
||||
@@ -92,173 +93,21 @@ export const PricingPlans = ({
|
||||
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);
|
||||
console.log("error getting sku", e);
|
||||
}
|
||||
}, [promo?.promoCode]);
|
||||
|
||||
const getPromo = async (code: string) => {
|
||||
try {
|
||||
let skuId: string;
|
||||
if (code.startsWith("com.streetwriters.notesnook")) {
|
||||
skuId = code;
|
||||
} else {
|
||||
skuId = await db.offers?.getCode(code.split(":")[0], Platform.OS);
|
||||
}
|
||||
|
||||
const products = await PremiumService.getProducts();
|
||||
const product = products.find((p) => p.productId === skuId);
|
||||
if (!product) return false;
|
||||
const isMonthly = product.productId.indexOf(".mo") > -1;
|
||||
|
||||
const cycleText = isMonthly
|
||||
? promoCyclesMonthly[
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid)
|
||||
.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesMonthly
|
||||
]
|
||||
: promoCyclesYearly[
|
||||
(Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid)
|
||||
.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0].billingCycleCount
|
||||
: parseInt(
|
||||
(product as RNIap.SubscriptionIOS)
|
||||
.introductoryPriceNumberOfPeriodsIOS as string
|
||||
)) as keyof typeof promoCyclesYearly
|
||||
];
|
||||
|
||||
setProduct({
|
||||
type: "promo",
|
||||
offerType: isMonthly ? "monthly" : "yearly",
|
||||
data: product,
|
||||
cycleText: cycleText,
|
||||
info: `Pay ${isMonthly ? "monthly" : "yearly"}, cancel anytime`
|
||||
});
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log("PROMOCODE ERROR:", code, e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSkus();
|
||||
}, [getSkus]);
|
||||
|
||||
const buySubscription = async (product: RNIap.Subscription) => {
|
||||
if (buying || !product) return;
|
||||
setBuying(true);
|
||||
try {
|
||||
if (!user) {
|
||||
setBuying(false);
|
||||
return;
|
||||
}
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
const androidOfferToken =
|
||||
Platform.OS === "android"
|
||||
? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
|
||||
.offerToken
|
||||
: null;
|
||||
|
||||
DatabaseLogger.info(
|
||||
`Subscription Requested initiated for user ${toUUID(user.id)}`
|
||||
);
|
||||
|
||||
await RNIap.requestSubscription({
|
||||
sku: product?.productId,
|
||||
obfuscatedAccountIdAndroid: user.id,
|
||||
obfuscatedProfileIdAndroid: user.id,
|
||||
appAccountToken: toUUID(user.id),
|
||||
andDangerouslyFinishTransactionAutomaticallyIOS: false,
|
||||
subscriptionOffers: androidOfferToken
|
||||
? [
|
||||
{
|
||||
offerToken: androidOfferToken,
|
||||
sku: product?.productId
|
||||
}
|
||||
]
|
||||
: undefined
|
||||
});
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
setBuying(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
await sleep(500);
|
||||
presentSheet({
|
||||
title: "Thank you for subscribing!",
|
||||
paragraph:
|
||||
"Your Notesnook Pro subscription will be activated soon. If your account is not upgraded to Notesnook Pro, your money will be refunded to you. In case of any issues, please reach out to us at support@streetwriters.co",
|
||||
action: async () => {
|
||||
eSendEvent(eCloseSheet);
|
||||
},
|
||||
icon: "check",
|
||||
actionText: "Continue"
|
||||
});
|
||||
} catch (e) {
|
||||
setBuying(false);
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
function getStandardPrice() {
|
||||
if (!product) return;
|
||||
const productType = product.offerType;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const pricingPhaseListItem = (product.data as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[1];
|
||||
|
||||
if (!pricingPhaseListItem) {
|
||||
const product =
|
||||
productType === "monthly"
|
||||
? monthlyPlan?.product
|
||||
: yearlyPlan?.product;
|
||||
return (product as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList?.[0]
|
||||
?.formattedPrice;
|
||||
}
|
||||
|
||||
return pricingPhaseListItem?.formattedPrice;
|
||||
} else {
|
||||
const productDefault =
|
||||
productType === "monthly" ? monthlyPlan?.product : yearlyPlan?.product;
|
||||
return (
|
||||
(product.data as RNIap.SubscriptionIOS)?.localizedPrice ||
|
||||
(productDefault as RNIap.SubscriptionIOS)?.localizedPrice
|
||||
);
|
||||
}
|
||||
}
|
||||
const {
|
||||
buySubscription,
|
||||
buying,
|
||||
getStandardPrice,
|
||||
product,
|
||||
setBuying,
|
||||
loading,
|
||||
user,
|
||||
setProduct,
|
||||
monthlyPlan,
|
||||
yearlyPlan,
|
||||
getPromo
|
||||
} = usePricingPlans({
|
||||
promoOffer: promo
|
||||
});
|
||||
|
||||
return loading ? (
|
||||
<View
|
||||
@@ -278,354 +127,215 @@ export const PricingPlans = ({
|
||||
}}
|
||||
>
|
||||
{buying ? (
|
||||
<BaseDialog statusBarTranslucent centered>
|
||||
<BaseDialog visible statusBarTranslucent centered>
|
||||
<ActivityIndicator size={50} color="white" />
|
||||
</BaseDialog>
|
||||
) : null}
|
||||
|
||||
{!upgrade ? (
|
||||
{!user && !product ? (
|
||||
<>
|
||||
<Paragraph
|
||||
style={{
|
||||
alignSelf: "center"
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
>
|
||||
{(Platform.OS === "android"
|
||||
? (monthlyPlan?.product as RNIap.SubscriptionAndroid | undefined)
|
||||
?.subscriptionOfferDetails[0]?.pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (monthlyPlan?.product as RNIap.SubscriptionIOS | undefined)
|
||||
?.localizedPrice) ||
|
||||
(PremiumService.getMontlySub() as any)?.localizedPrice}
|
||||
/ mo
|
||||
</Paragraph>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setUpgrade(true);
|
||||
}}
|
||||
title={"Upgrade now"}
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 15,
|
||||
marginTop: 15,
|
||||
borderRadius: 100
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onPress={async () => {
|
||||
try {
|
||||
await db.user?.activateTrial();
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
await sleep(300);
|
||||
Walkthrough.present("trialstarted", false, true);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
title={"Try free for 14 days"}
|
||||
type="secondaryAccented"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 15
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{product?.type === "promo" ? (
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 15,
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{product?.offerType === "monthly" ? (
|
||||
<PricingItem
|
||||
product={{
|
||||
type: "monthly",
|
||||
data: monthlyPlan?.product,
|
||||
info: "Pay once a month, cancel anytime."
|
||||
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<>
|
||||
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: marginTop || 20,
|
||||
marginBottom: 20
|
||||
}}
|
||||
strikethrough={true}
|
||||
/>
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Get {monthlyPlan?.info?.discount}% off in{" "}
|
||||
{monthlyPlan?.info?.country}
|
||||
</Heading>
|
||||
</View>
|
||||
) : (
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!monthlyPlan?.product) return;
|
||||
buySubscription(monthlyPlan?.product);
|
||||
}}
|
||||
product={{
|
||||
type: "yearly",
|
||||
data: yearlyPlan?.product,
|
||||
info: "Pay once a year, cancel anytime."
|
||||
}}
|
||||
strikethrough={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Heading
|
||||
style={{
|
||||
paddingTop: 15,
|
||||
fontSize: SIZE.lg
|
||||
}}
|
||||
>
|
||||
Special offer for you
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 20,
|
||||
paddingBottom: 10
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
marginTop: marginTop || 20,
|
||||
marginBottom: 20
|
||||
}}
|
||||
size={SIZE.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}
|
||||
Choose a plan
|
||||
</Heading>
|
||||
{product?.cycleText ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
color: colors.secondary.paragraph,
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.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: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
context: "local",
|
||||
input: true,
|
||||
inputPlaceholder: "Enter code",
|
||||
positiveText: "Apply",
|
||||
positivePress: async (value) => {
|
||||
if (!value) return;
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
setBuying(true);
|
||||
try {
|
||||
if (!(await getPromo(value as string)))
|
||||
throw new Error("Error applying promo code");
|
||||
ToastManager.show({
|
||||
heading: "Discount applied!",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
setBuying(false);
|
||||
} catch (e) {
|
||||
setBuying(false);
|
||||
ToastManager.show({
|
||||
heading: "Promo code invalid or expired",
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
},
|
||||
title: "Have a promo code?",
|
||||
paragraph:
|
||||
"Enter your promo code to get a special discount."
|
||||
});
|
||||
}}
|
||||
title="I have a promo code"
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
height: 15
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<View>
|
||||
{!user ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
setTimeout(() => {
|
||||
eSendEvent(eOpenLoginDialog, 1);
|
||||
}, 400);
|
||||
}}
|
||||
title={"Sign up for free"}
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: product?.type === "promo" ? 0 : 30,
|
||||
marginBottom: 10
|
||||
}}
|
||||
/>
|
||||
{Platform.OS !== "ios" &&
|
||||
promo &&
|
||||
!promo.promoCode.startsWith("com.streetwriters.notesnook") ? (
|
||||
<Paragraph
|
||||
size={SIZE.md}
|
||||
textBreakStrategy="balanced"
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
justifyContent: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
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"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
setProduct(undefined);
|
||||
}}
|
||||
style={{
|
||||
marginTop: 5
|
||||
}}
|
||||
height={30}
|
||||
fontSize={13}
|
||||
type="errorShade"
|
||||
title="Cancel promo code"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: !compact ? "column" : "row",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-around"
|
||||
}}
|
||||
>
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!monthlyPlan?.product) return;
|
||||
buySubscription(monthlyPlan?.product);
|
||||
}}
|
||||
compact={compact}
|
||||
product={{
|
||||
type: "monthly",
|
||||
data: monthlyPlan?.product,
|
||||
info: "Pay once a month, cancel anytime."
|
||||
}}
|
||||
/>
|
||||
|
||||
{!compact && (
|
||||
<View
|
||||
style={{
|
||||
height: 1,
|
||||
marginVertical: 5
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!yearlyPlan?.product) return;
|
||||
buySubscription(yearlyPlan?.product);
|
||||
}}
|
||||
compact={compact}
|
||||
product={{
|
||||
type: "yearly",
|
||||
data: yearlyPlan?.product,
|
||||
info: "Pay once a year, cancel anytime."
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{Platform.OS !== "ios" ? (
|
||||
<Button
|
||||
height={35}
|
||||
style={{
|
||||
marginTop: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
presentDialog({
|
||||
context: "local",
|
||||
input: true,
|
||||
inputPlaceholder: "Enter code",
|
||||
positiveText: "Apply",
|
||||
positivePress: async (value) => {
|
||||
if (!value) return;
|
||||
eSendEvent(eCloseSimpleDialog);
|
||||
setBuying(true);
|
||||
try {
|
||||
if (!(await getPromo(value as string)))
|
||||
throw new Error("Error applying promo code");
|
||||
ToastManager.show({
|
||||
heading: "Discount applied!",
|
||||
type: "success",
|
||||
context: "local"
|
||||
});
|
||||
setBuying(false);
|
||||
} catch (e) {
|
||||
setBuying(false);
|
||||
ToastManager.show({
|
||||
heading: "Promo code invalid or expired",
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
}
|
||||
},
|
||||
title: "Have a promo code?",
|
||||
paragraph: "Enter your promo code to get a special discount."
|
||||
});
|
||||
}}
|
||||
title="I have a promo code"
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
height: 15
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<View>
|
||||
{!user ? (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
eSendEvent(eClosePremiumDialog);
|
||||
eSendEvent(eCloseSheet);
|
||||
setTimeout(() => {
|
||||
eSendEvent(eOpenLoginDialog, 1);
|
||||
}, 400);
|
||||
}}
|
||||
title={"Sign up for free"}
|
||||
type="accent"
|
||||
width={250}
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
marginTop: product?.type === "promo" ? 0 : 30,
|
||||
marginBottom: 10
|
||||
}}
|
||||
/>
|
||||
{Platform.OS !== "ios" &&
|
||||
promo &&
|
||||
!promo.promoCode.startsWith("com.streetwriters.notesnook") ? (
|
||||
<Paragraph
|
||||
size={SIZE.md}
|
||||
textBreakStrategy="balanced"
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
justifyContent: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
Use promo code{" "}
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: "OpenSans-SemiBold"
|
||||
}}
|
||||
>
|
||||
{promo.promoCode}
|
||||
</Text>{" "}
|
||||
at checkout
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (!product?.data) return;
|
||||
buySubscription(product.data);
|
||||
}}
|
||||
height={40}
|
||||
width="50%"
|
||||
type="accent"
|
||||
title="Subscribe now"
|
||||
/>
|
||||
|
||||
<Button
|
||||
onPress={() => {
|
||||
setProduct(undefined);
|
||||
}}
|
||||
style={{
|
||||
marginTop: 5
|
||||
}}
|
||||
height={30}
|
||||
fontSize={13}
|
||||
type="errorShade"
|
||||
title="Cancel promo code"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{!user || !upgrade ? (
|
||||
{!user ? (
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
@@ -645,7 +355,7 @@ export const PricingPlans = ({
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{user && upgrade ? (
|
||||
{user ? (
|
||||
<>
|
||||
{Platform.OS === "ios" ? (
|
||||
<Paragraph
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ProTag = ({ width, size, background }) => {
|
||||
marginRight: 3
|
||||
}}
|
||||
size={size}
|
||||
color={colors.primary.accent}
|
||||
color={colors.static.orange}
|
||||
name="crown"
|
||||
/>
|
||||
<Paragraph size={size - 1.5} color={colors.primary.accent}>
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Color, Note } from "@notesnook/core/dist/types";
|
||||
import { Color, Note } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
371
apps/mobile/app/components/sheets/buy-plan/index.tsx
Normal file
371
apps/mobile/app/components/sheets/buy-plan/index.tsx
Normal file
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React from "react";
|
||||
import {
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View
|
||||
} from "react-native";
|
||||
import usePricingPlans from "../../../hooks/use-pricing-plans";
|
||||
import { presentSheet } from "../../../services/event-manager";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { Button } from "../../ui/button";
|
||||
import dayjs from "dayjs";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
import { IconButton } from "../../ui/icon-button";
|
||||
import useGlobalSafeAreaInsets from "../../../hooks/use-global-safe-area-insets";
|
||||
|
||||
export const BuyPlan = (props: {
|
||||
planId: string;
|
||||
productId: string;
|
||||
canActivateTrial?: boolean;
|
||||
goBack: () => void;
|
||||
goNext: () => void;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const pricingPlans = usePricingPlans({
|
||||
planId: props.planId,
|
||||
productId: props.productId,
|
||||
onBuy: () => {
|
||||
props.goNext();
|
||||
}
|
||||
});
|
||||
|
||||
const billingDuration = pricingPlans.getBillingDuration(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription,
|
||||
0,
|
||||
0,
|
||||
true
|
||||
);
|
||||
const is5YearPlanSelected =
|
||||
pricingPlans.selectedProduct?.productId.includes("5year");
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
gap: 16,
|
||||
paddingBottom: 80,
|
||||
paddingTop: Platform.OS === "android" ? insets.top : 0
|
||||
}}
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
stickyHeaderIndices={[0]}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
backgroundColor: colors.primary.background
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
name="chevron-left"
|
||||
onPress={() => {
|
||||
props.goBack();
|
||||
}}
|
||||
style={{
|
||||
alignSelf: "flex-start"
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
gap: 16
|
||||
}}
|
||||
>
|
||||
<Heading
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
{props.canActivateTrial
|
||||
? `Try ${pricingPlans.currentPlan?.name} plan for free`
|
||||
: `${pricingPlans.currentPlan?.name} plan`}{" "}
|
||||
</Heading>
|
||||
|
||||
{props.canActivateTrial ? (
|
||||
<View
|
||||
style={{
|
||||
gap: 10,
|
||||
marginBottom: 10
|
||||
}}
|
||||
>
|
||||
{(is5YearPlanSelected
|
||||
? [
|
||||
"One time purchase, no auto-renewal",
|
||||
"Pay once and use for 5 years"
|
||||
]
|
||||
: [
|
||||
`Free ${billingDuration.duration} day trial, cancel any time`,
|
||||
"Google will remind you before your trial ends"
|
||||
]
|
||||
).map((item) => (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
}}
|
||||
key={item}
|
||||
>
|
||||
<Icon
|
||||
color={colors.primary.accent}
|
||||
size={SIZE.lg}
|
||||
name="check"
|
||||
/>
|
||||
<Paragraph>{item}</Paragraph>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{[
|
||||
`notesnook.${props.planId}.yearly`,
|
||||
`notesnook.${props.planId}.monthly`,
|
||||
...(props.planId === "essential"
|
||||
? []
|
||||
: [`notesnook.${props.planId}.5year`])
|
||||
].map((item) => (
|
||||
<ProductItem
|
||||
key={item}
|
||||
pricingPlans={pricingPlans}
|
||||
productId={item}
|
||||
/>
|
||||
))}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
marginTop: 20
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.primary.paragraph} size={SIZE.sm}>
|
||||
Due today{" "}
|
||||
{pricingPlans.userCanRequestTrial ? (
|
||||
<Text
|
||||
style={{
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
({billingDuration.duration} days free)
|
||||
</Text>
|
||||
) : null}
|
||||
</Heading>
|
||||
|
||||
<Paragraph color={colors.primary.paragraph}>
|
||||
{pricingPlans.userCanRequestTrial
|
||||
? "0.00"
|
||||
: pricingPlans.getStandardPrice(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
{pricingPlans.userCanRequestTrial ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
Due{" "}
|
||||
{dayjs().add(billingDuration.duration, "day").format("DD MMMM")}
|
||||
</Paragraph>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
{pricingPlans.getStandardPrice(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription
|
||||
)}
|
||||
</Paragraph>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<Button
|
||||
width="100%"
|
||||
type="accent"
|
||||
loading={pricingPlans.loading}
|
||||
title={
|
||||
pricingPlans.selectedProduct?.productId.includes("5year")
|
||||
? "Purchase"
|
||||
: pricingPlans?.userCanRequestTrial
|
||||
? "Subscribe and start free trial"
|
||||
: "Subscribe"
|
||||
}
|
||||
onPress={() => {
|
||||
const offerToken = pricingPlans.getOfferTokenAndroid(
|
||||
pricingPlans.selectedProduct as RNIap.SubscriptionAndroid,
|
||||
0
|
||||
);
|
||||
pricingPlans.subscribe(
|
||||
pricingPlans.selectedProduct as RNIap.Subscription,
|
||||
offerToken
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<View>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
{is5YearPlanSelected
|
||||
? `This is a one time purchase, no subscription.`
|
||||
: `Cancel anytime, subscription auto-renews.`}
|
||||
</Heading>
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
By joining you agree to our{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/privacy");
|
||||
}}
|
||||
>
|
||||
privacy policy
|
||||
</Text>{" "}
|
||||
and{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
onPress={() => {
|
||||
openLinkInBrowser("https://notesnook.com/tos");
|
||||
}}
|
||||
>
|
||||
terms of use
|
||||
</Text>
|
||||
.
|
||||
</Heading>
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
const ProductItem = (props: {
|
||||
pricingPlans: ReturnType<typeof usePricingPlans>;
|
||||
productId: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const product =
|
||||
props.pricingPlans?.currentPlan?.subscriptions?.[props.productId] ||
|
||||
props.pricingPlans?.currentPlan?.products?.[props.productId];
|
||||
const isAnnual = product?.productId.includes("yearly");
|
||||
const isSelected =
|
||||
product?.productId === props.pricingPlans.selectedProduct?.productId;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: 10
|
||||
}}
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
props.pricingPlans.selectProduct(product?.productId);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={isSelected ? "radiobox-marked" : "radiobox-blank"}
|
||||
color={isSelected ? colors.primary.accent : colors.secondary.icon}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Heading size={SIZE.md}>
|
||||
{isAnnual
|
||||
? "Yearly"
|
||||
: product?.productId.includes("5year")
|
||||
? "5 year plan (One time purchase)"
|
||||
: "Monthly"}
|
||||
</Heading>
|
||||
|
||||
{isAnnual ? (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.static.red,
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 6,
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.static.white} size={SIZE.xs}>
|
||||
Best value -{" "}
|
||||
{props.pricingPlans.compareProductPrice(
|
||||
props.pricingPlans.currentPlan?.id as string,
|
||||
`notesnook.${props.pricingPlans.currentPlan?.id}.yearly`,
|
||||
`notesnook.${props.pricingPlans.currentPlan?.id}.monthly`
|
||||
)}
|
||||
% Off
|
||||
</Heading>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<Paragraph>
|
||||
{product?.productId.includes("5year")
|
||||
? (product as RNIap.Product).localizedPrice
|
||||
: props.pricingPlans.getStandardPrice(
|
||||
product as RNIap.Subscription
|
||||
)}{" "}
|
||||
{isAnnual || product?.productId.includes("5year")
|
||||
? `(${props.pricingPlans.getPrice(
|
||||
product as RNIap.Subscription,
|
||||
props.pricingPlans.hasTrialOffer() ? 1 : 0,
|
||||
isAnnual
|
||||
)}/month)`
|
||||
: null}
|
||||
</Paragraph>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { Note } from "@notesnook/core";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { EVENTS } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
@@ -17,36 +17,34 @@ 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 { Debug } from "@notesnook/core/dist/api/debug";
|
||||
import { Debug } from "@notesnook/core";
|
||||
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Linking, Platform, Text, TextInput, View } from "react-native";
|
||||
import { getVersion } from "react-native-device-info";
|
||||
import { useStoredRef } from "../../../hooks/use-stored-ref";
|
||||
import { ToastManager, eSendEvent } from "../../../services/event-manager";
|
||||
import { ToastManager } from "../../../services/event-manager";
|
||||
import PremiumService from "../../../services/premium";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { openLinkInBrowser } from "../../../utils/functions";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { sleep } from "../../../utils/time";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import Heading from "../../ui/typography/heading";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
|
||||
export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
const body = useStoredRef("issueBody", defaultBody);
|
||||
const body = useStoredRef("issueBody");
|
||||
const title = useStoredRef("issueTitle", defaultTitle);
|
||||
|
||||
const [done, setDone] = useState(false);
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const bodyRef = useRef();
|
||||
const initialLayout = useRef(false);
|
||||
const issueUrl = useRef();
|
||||
|
||||
const onPress = async () => {
|
||||
if (loading) return;
|
||||
@@ -56,57 +54,24 @@ export const Issue = ({ defaultTitle, defaultBody, issueTitle }) => {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
let issue_url = await Debug.report({
|
||||
issueUrl.current = await Debug.report({
|
||||
title: title.current,
|
||||
body:
|
||||
body.current +
|
||||
`\n${defaultBody || ""}` +
|
||||
`\n_______________
|
||||
**Device information:**
|
||||
App version: ${getVersion()}
|
||||
Platform: ${Platform.OS}
|
||||
Model: ${Platform.constants.Brand || ""}-${Platform.constants.Model || ""}-${
|
||||
Platform.constants.Version || ""
|
||||
}
|
||||
Device: ${getBrand() || ""}-${getModel() || ""}-${getSystemVersion() || ""}
|
||||
Pro: ${PremiumService.get()}
|
||||
Logged in: ${user ? "yes" : "no"}`,
|
||||
userId: user?.id
|
||||
});
|
||||
setLoading(false);
|
||||
eSendEvent(eCloseSheet);
|
||||
body.reset();
|
||||
title.reset();
|
||||
await sleep(300);
|
||||
presentDialog({
|
||||
title: "Issue reported",
|
||||
paragraph: (
|
||||
<Text>
|
||||
You can track your issue at{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(issue_url);
|
||||
}}
|
||||
>
|
||||
{issue_url}.
|
||||
</Text>{" "}
|
||||
Please note that we will respond to your issue on the given link. We
|
||||
recommend that you save it.
|
||||
</Text>
|
||||
),
|
||||
positiveText: "Copy link",
|
||||
positivePress: () => {
|
||||
Clipboard.setString(issue_url);
|
||||
ToastManager.show({
|
||||
heading: "Issue url copied!",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
},
|
||||
negativeText: "Close"
|
||||
});
|
||||
setDone(true);
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
@@ -124,125 +89,177 @@ Logged in: ${user ? "yes" : "no"}`,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title={issueTitle || "Report issue"}
|
||||
paragraph={
|
||||
issueTitle
|
||||
? "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap."
|
||||
: "Let us know if you have faced any issue/bug while using Notesnook."
|
||||
}
|
||||
/>
|
||||
{done ? (
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
height: 250,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Heading>Issue submitted</Heading>
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
selectable={true}
|
||||
>
|
||||
You can track your issue at{" "}
|
||||
<Paragraph
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={() => {
|
||||
Linking.openURL(issueUrl.current);
|
||||
}}
|
||||
>
|
||||
{issueUrl.current}
|
||||
</Paragraph>
|
||||
. Please note that we will respond to your issue on the given
|
||||
link. We recommend that you save it.
|
||||
</Paragraph>
|
||||
|
||||
<Seperator half />
|
||||
|
||||
<TextInput
|
||||
placeholder="Title"
|
||||
onChangeText={(v) => (title.current = v)}
|
||||
defaultValue={title.current}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
marginBottom: 10,
|
||||
fontSize: SIZE.md,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
ref={bodyRef}
|
||||
placeholder={`Tell us more about the issue you are facing.
|
||||
|
||||
For example:
|
||||
- Steps to reproduce the issue
|
||||
- Things you have tried etc.`}
|
||||
multiline
|
||||
numberOfLines={5}
|
||||
textAlignVertical="top"
|
||||
onChangeText={(v) => (body.current = v)}
|
||||
onLayout={() => {
|
||||
if (initialLayout.current) return;
|
||||
initialLayout.current = true;
|
||||
if (body.current) {
|
||||
bodyRef.current?.setNativeProps({
|
||||
text: body.current,
|
||||
selection: {
|
||||
start: 0,
|
||||
end: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
maxHeight: 200,
|
||||
fontSize: SIZE.sm,
|
||||
marginBottom: 2.5,
|
||||
color: colors.primary.paragraph
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>{`App version: ${getVersion()} Platform: ${Platform.OS} Model: ${
|
||||
Platform.constants.Brand
|
||||
}-${Platform.constants.Model}-${Platform.constants.Version}`}</Paragraph>
|
||||
|
||||
<Seperator />
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={loading ? null : "Submit"}
|
||||
loading={loading}
|
||||
width="100%"
|
||||
type="accent"
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
The information above will be publically available at{" "}
|
||||
<Text
|
||||
onPress={() => {
|
||||
Linking.openURL("https://github.com/streetwriters/notesnook");
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
github.com/streetwriters/notesnook.
|
||||
</Text>{" "}
|
||||
If you want to ask something in general or need some assistance, we
|
||||
would suggest that you{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser("https://discord.gg/zQBK97EE22", colors);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
<Button
|
||||
title="Open issue"
|
||||
onPress={() => {
|
||||
Linking.openURL(issueUrl.current);
|
||||
}}
|
||||
type="accent"
|
||||
width="100%"
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DialogHeader
|
||||
title={issueTitle || "Report issue"}
|
||||
paragraph={
|
||||
issueTitle
|
||||
? "We are sorry, it seems that the app crashed due to an error. You can submit a bug report below so we can fix this asap."
|
||||
: "Let us know if you have faced any issue/bug while using Notesnook."
|
||||
}
|
||||
}}
|
||||
>
|
||||
join our community on Discord.
|
||||
</Text>
|
||||
</Paragraph>
|
||||
/>
|
||||
|
||||
<Seperator half />
|
||||
|
||||
<TextInput
|
||||
placeholder="Title"
|
||||
onChangeText={(v) => (title.current = v)}
|
||||
defaultValue={title.current}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
marginBottom: 10,
|
||||
fontSize: SIZE.md,
|
||||
color: colors.primary.heading
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
ref={bodyRef}
|
||||
multiline
|
||||
placeholder={`Tell us more about the issue you are facing.
|
||||
|
||||
For example:
|
||||
- What were you trying to do in the app?
|
||||
- What did you expect to happen?
|
||||
- Steps to reproduce the issue
|
||||
- Things you have tried etc.`}
|
||||
numberOfLines={5}
|
||||
textAlignVertical="top"
|
||||
onChangeText={(v) => (body.current = v)}
|
||||
onLayout={() => {
|
||||
if (initialLayout.current) return;
|
||||
initialLayout.current = true;
|
||||
if (body.current) {
|
||||
bodyRef.current?.setNativeProps({
|
||||
text: body.current,
|
||||
selection: {
|
||||
start: 0,
|
||||
end: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 5,
|
||||
padding: 12,
|
||||
fontFamily: "OpenSans-Regular",
|
||||
maxHeight: 200,
|
||||
fontSize: SIZE.sm,
|
||||
marginBottom: 2.5,
|
||||
color: colors.primary.paragraph
|
||||
}}
|
||||
placeholderTextColor={colors.primary.placeholder}
|
||||
/>
|
||||
<Paragraph
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
>{`App version: ${getVersion()} Platform: ${
|
||||
Platform.OS
|
||||
} Model: ${getBrand()}-${getModel()}-${getSystemVersion()}`}</Paragraph>
|
||||
|
||||
<Seperator />
|
||||
<Button
|
||||
onPress={onPress}
|
||||
title={loading ? null : "Submit"}
|
||||
loading={loading}
|
||||
width="100%"
|
||||
type="accent"
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
The information above will be publically available at{" "}
|
||||
<Text
|
||||
onPress={() => {
|
||||
Linking.openURL("https://github.com/streetwriters/notesnook");
|
||||
}}
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
>
|
||||
github.com/streetwriters/notesnook.
|
||||
</Text>{" "}
|
||||
If you want to ask something in general or need some assistance, we
|
||||
would suggest that you{" "}
|
||||
<Text
|
||||
style={{
|
||||
textDecorationLine: "underline",
|
||||
color: colors.primary.accent
|
||||
}}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await openLinkInBrowser(
|
||||
"https://discord.gg/zQBK97EE22",
|
||||
colors
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
join our community on Discord.
|
||||
</Text>
|
||||
</Paragraph>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ import { Button } from "../../ui/button";
|
||||
import Input from "../../ui/input";
|
||||
import { Pressable } from "../../ui/pressable";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
|
||||
import type { LinkAttributes } from "@notesnook/editor";
|
||||
import {
|
||||
EditorEvents,
|
||||
editorController
|
||||
|
||||
@@ -18,8 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Tags } from "@notesnook/core/dist/collections/tags";
|
||||
import { Tag } from "@notesnook/core/dist/types";
|
||||
import { sanitizeTag } from "@notesnook/core";
|
||||
import { Tag } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, {
|
||||
RefObject,
|
||||
@@ -268,8 +268,8 @@ const ManageTagsSheet = (props: {
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(v) => {
|
||||
setQuery(Tags.sanitize(v));
|
||||
checkQueryExists(Tags.sanitize(v));
|
||||
setQuery(sanitizeTag(v));
|
||||
checkQueryExists(sanitizeTag(v));
|
||||
}}
|
||||
onFocusInput={() => {
|
||||
setFocus(true);
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { EVENTS } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
@@ -42,10 +42,7 @@ import { ProgressBarComponent } from "../../ui/svg/lazy";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { Issue } from "../github/issue";
|
||||
|
||||
export const makeError = (
|
||||
stack: string,
|
||||
component: string
|
||||
) => `Please let us know what happened. What steps we can take to reproduce the issue here.
|
||||
export const makeError = (stack: string, component: string) => `
|
||||
|
||||
_______________________________
|
||||
Stacktrace: In ${component}::${stack}`;
|
||||
@@ -98,9 +95,9 @@ export default function Migrate() {
|
||||
await sleep(1);
|
||||
const { error, report } = await BackupService.run(false, "local");
|
||||
if (error) {
|
||||
ToastManager.error(error, "Backup failed");
|
||||
ToastManager.error(error as Error, "Backup failed");
|
||||
if (report) {
|
||||
reportError(error);
|
||||
reportError(error as Error);
|
||||
}
|
||||
setLoading(false);
|
||||
return;
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Note, Notebook } from "@notesnook/core/dist/types";
|
||||
import { Note, Notebook } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, TextInput, View } from "react-native";
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
createInternalLink,
|
||||
highlightInternalLinks
|
||||
} from "@notesnook/core";
|
||||
import { ContentBlock, ItemReference, Note } from "@notesnook/core/dist/types";
|
||||
import { ContentBlock, ItemReference, Note } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Item, ItemReference } from "@notesnook/core/dist/types";
|
||||
import { Item, ItemReference } from "@notesnook/core";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
@@ -1,600 +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 { getFormattedDate } from "@notesnook/common";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { FlatList } from "react-native-actions-sheet";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import DocumentPicker from "react-native-document-picker";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
import { unzip } from "react-native-zip-archive";
|
||||
import { db } from "../../../common/database";
|
||||
import storage from "../../../common/database/storage";
|
||||
import { cacheDir, copyFileAsync } from "../../../common/filesystem/utils";
|
||||
import {
|
||||
ToastManager,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import Navigation from "../../../services/navigation";
|
||||
import SettingsService from "../../../services/settings";
|
||||
import { refreshAllStores } from "../../../stores/create-db-collection-store";
|
||||
import { eCloseRestoreDialog, eOpenRestoreDialog } from "../../../utils/events";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Dialog } from "../../dialog";
|
||||
import DialogHeader from "../../dialog/dialog-header";
|
||||
import { presentDialog } from "../../dialog/functions";
|
||||
import { Toast } from "../../toast";
|
||||
import { Button } from "../../ui/button";
|
||||
import Seperator from "../../ui/seperator";
|
||||
import SheetWrapper from "../../ui/sheet";
|
||||
import Paragraph from "../../ui/typography/paragraph";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
|
||||
const RestoreDataSheet = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [restoring, setRestoring] = useState(false);
|
||||
const sheet = useRef();
|
||||
useEffect(() => {
|
||||
const open = async () => {
|
||||
setVisible(true);
|
||||
setTimeout(() => {
|
||||
sheet.current?.show();
|
||||
}, 1);
|
||||
};
|
||||
eSubscribeEvent(eOpenRestoreDialog, open);
|
||||
eSubscribeEvent(eCloseRestoreDialog, close);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eOpenRestoreDialog, open);
|
||||
eUnSubscribeEvent(eCloseRestoreDialog, close);
|
||||
};
|
||||
}, [close]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (restoring) {
|
||||
showIsWorking();
|
||||
return;
|
||||
}
|
||||
sheet.current?.hide();
|
||||
setTimeout(() => {
|
||||
setVisible(false);
|
||||
}, 150);
|
||||
}, [restoring]);
|
||||
|
||||
const showIsWorking = () => {
|
||||
ToastManager.show({
|
||||
heading: "Restoring Backup",
|
||||
message: "Your backup data is being restored. please wait.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
return !visible ? null : (
|
||||
<SheetWrapper
|
||||
fwdRef={sheet}
|
||||
gestureEnabled={!restoring}
|
||||
closeOnTouchBackdrop={!restoring}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<RestoreDataComponent
|
||||
close={close}
|
||||
restoring={restoring}
|
||||
setRestoring={setRestoring}
|
||||
actionSheetRef={sheet}
|
||||
/>
|
||||
<Toast context="local" />
|
||||
</SheetWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestoreDataSheet;
|
||||
|
||||
const RestoreDataComponent = ({ close, setRestoring, restoring }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [files, setFiles] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [backupDirectoryAndroid, setBackupDirectoryAndroid] = useState(false);
|
||||
const [progress, setProgress] = useState();
|
||||
useEffect(() => {
|
||||
const subscription = db.eventManager.subscribe(
|
||||
EVENTS.migrationProgress,
|
||||
(progress) => {
|
||||
setProgress(progress);
|
||||
}
|
||||
);
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
checkBackups();
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const restore = async (item) => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const file = Platform.OS === "ios" ? item.path : item.uri;
|
||||
console.log(file);
|
||||
if (file.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const cacheFile = `file://${RNFetchBlob.fs.dirs.CacheDir}/backup.zip`;
|
||||
if (await RNFetchBlob.fs.exists(cacheFile)) {
|
||||
await RNFetchBlob.fs.unlink(cacheFile);
|
||||
}
|
||||
await RNFetchBlob.fs.createFile(cacheFile, "", "utf8");
|
||||
console.log("copying");
|
||||
await copyFileAsync(file, cacheFile);
|
||||
console.log("copied");
|
||||
await restoreFromZip(cacheFile);
|
||||
} else {
|
||||
await restoreFromZip(file, false);
|
||||
}
|
||||
} else if (file.endsWith(".nnbackup")) {
|
||||
let backup;
|
||||
if (Platform.OS === "android") {
|
||||
backup = await ScopedStorage.readFile(file, "utf8");
|
||||
} else {
|
||||
backup = await RNFetchBlob.fs.readFile(file, "utf8");
|
||||
}
|
||||
await restoreFromNNBackup(JSON.parse(backup));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const withPassword = () => {
|
||||
return new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
presentDialog({
|
||||
context: "local",
|
||||
title: "Encrypted backup",
|
||||
input: true,
|
||||
inputPlaceholder: "Password",
|
||||
paragraph: "Please enter password of this backup file",
|
||||
positiveText: "Restore",
|
||||
secureTextEntry: true,
|
||||
onClose: () => {
|
||||
if (resolved) return;
|
||||
resolve(undefined);
|
||||
},
|
||||
negativeText: "Cancel",
|
||||
positivePress: async (password, isEncryptionKey) => {
|
||||
resolve({
|
||||
value: password,
|
||||
isEncryptionKey
|
||||
});
|
||||
resolved = true;
|
||||
return true;
|
||||
},
|
||||
check: {
|
||||
info: "Use encryption key",
|
||||
type: "transparent"
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const checkBackups = async () => {
|
||||
try {
|
||||
let files = [];
|
||||
if (Platform.OS === "android") {
|
||||
let backupDirectory = SettingsService.get().backupDirectoryAndroid;
|
||||
if (backupDirectory) {
|
||||
setBackupDirectoryAndroid(backupDirectory);
|
||||
files = await ScopedStorage.listFiles(backupDirectory.uri);
|
||||
} else {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let path = await storage.checkAndCreateDir("/backups/");
|
||||
files = await RNFetchBlob.fs.lstat(path);
|
||||
}
|
||||
files = files
|
||||
.filter((file) => {
|
||||
const name = Platform.OS === "android" ? file.name : file.filename;
|
||||
return name.endsWith(".nnbackup") || name.endsWith(".nnbackupz");
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
let timeA = a.lastModified;
|
||||
let timeB = b.lastModified;
|
||||
return timeB - timeA;
|
||||
});
|
||||
setFiles(files);
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const restoreBackup = async (backup, password, key) => {
|
||||
await db.transaction(async () => {
|
||||
await db.backup.import(backup, password, key);
|
||||
});
|
||||
await db.initCollections();
|
||||
refreshAllStores();
|
||||
ToastManager.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
Navigation.queueRoutesForUpdate();
|
||||
return true;
|
||||
};
|
||||
|
||||
const backupError = (e) => {
|
||||
console.log(e.stack);
|
||||
ToastManager.show({
|
||||
heading: "Restore failed",
|
||||
message:
|
||||
e.message ||
|
||||
"The selected backup data file is invalid. You must select a *.nnbackup file to restore.",
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromZip(file, remove) {
|
||||
try {
|
||||
const zipOutputFolder = `${cacheDir}/backup_extracted`;
|
||||
if (await RNFetchBlob.fs.exists(zipOutputFolder)) {
|
||||
await RNFetchBlob.fs.unlink(zipOutputFolder);
|
||||
await RNFetchBlob.fs.mkdir(zipOutputFolder);
|
||||
}
|
||||
await unzip(file, zipOutputFolder);
|
||||
console.log("Unzipped files successfully to", zipOutputFolder);
|
||||
|
||||
const backupFiles = await RNFetchBlob.fs.ls(zipOutputFolder);
|
||||
|
||||
// if (backupFiles.findIndex((file) => file === ".nnbackup") === -1) {
|
||||
// throw new Error("Backup file is invalid");
|
||||
// }
|
||||
|
||||
await db.transaction(async () => {
|
||||
let password;
|
||||
let key;
|
||||
console.log(
|
||||
`Found ${backupFiles?.length} files to restore from backup`
|
||||
);
|
||||
for (const path of backupFiles) {
|
||||
if (path === ".nnbackup") continue;
|
||||
const filePath = `${zipOutputFolder}/${path}`;
|
||||
const data = await RNFetchBlob.fs.readFile(filePath, "utf8");
|
||||
const parsed = JSON.parse(data);
|
||||
|
||||
if (parsed.encrypted && !password) {
|
||||
console.log("Backup is encrypted...", "requesting password");
|
||||
const { value, isEncryptionKey } = await withPassword();
|
||||
|
||||
if (isEncryptionKey) {
|
||||
key = value;
|
||||
} else {
|
||||
password = value;
|
||||
}
|
||||
if (!password && !key) throw new Error("Failed to decrypt backup");
|
||||
}
|
||||
await db.backup.import(parsed, password, key);
|
||||
console.log("Imported", path);
|
||||
}
|
||||
});
|
||||
// Remove files from cache
|
||||
RNFetchBlob.fs.unlink(zipOutputFolder).catch(console.log);
|
||||
if (remove) {
|
||||
RNFetchBlob.fs.unlink(file).catch(console.log);
|
||||
}
|
||||
|
||||
refreshAllStores();
|
||||
Navigation.queueRoutesForUpdate();
|
||||
setRestoring(false);
|
||||
close();
|
||||
ToastManager.show({
|
||||
heading: "Backup restored successfully.",
|
||||
type: "success",
|
||||
context: "global"
|
||||
});
|
||||
} catch (e) {
|
||||
backupError(e);
|
||||
setRestoring(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} file
|
||||
*/
|
||||
async function restoreFromNNBackup(backup) {
|
||||
try {
|
||||
if (backup.data.iv && backup.data.salt) {
|
||||
const { value, isEncryptionKey } = await withPassword();
|
||||
|
||||
let key;
|
||||
let password;
|
||||
if (isEncryptionKey) {
|
||||
key = value;
|
||||
} else {
|
||||
password = value;
|
||||
}
|
||||
|
||||
if (key || password) {
|
||||
try {
|
||||
await restoreBackup(backup, password, key);
|
||||
close();
|
||||
setRestoring(false);
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
} else {
|
||||
setRestoring(false);
|
||||
}
|
||||
} else {
|
||||
await restoreBackup(backup);
|
||||
setRestoring(false);
|
||||
close();
|
||||
}
|
||||
} catch (e) {
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
|
||||
const button = {
|
||||
title: "Restore from files",
|
||||
onPress: async () => {
|
||||
if (restoring) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: true
|
||||
});
|
||||
console.log("disabled...");
|
||||
const file = await DocumentPicker.pickSingle({
|
||||
copyTo: "cachesDirectory"
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
if (file.name.endsWith(".nnbackupz")) {
|
||||
setRestoring(true);
|
||||
await restoreFromZip(file.fileCopyUri, true);
|
||||
} else if (file.name.endsWith(".nnbackup")) {
|
||||
RNFetchBlob.fs.unlink(file.fileCopyUri).catch(console.log);
|
||||
setRestoring(true);
|
||||
const data = await fetch(file.uri);
|
||||
await restoreFromNNBackup(await data.json());
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error", e.stack);
|
||||
setTimeout(() => {
|
||||
useUserStore.setState({
|
||||
disableAppLockRequests: false
|
||||
});
|
||||
}, 1000);
|
||||
setRestoring(false);
|
||||
backupError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }) => (
|
||||
<View
|
||||
style={{
|
||||
minHeight: 50,
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
borderRadius: 0,
|
||||
flexDirection: "row",
|
||||
borderBottomWidth: 0.5,
|
||||
borderBottomColor: colors.primary.border
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
maxWidth: "75%"
|
||||
}}
|
||||
>
|
||||
<Paragraph size={SIZE.sm} style={{ width: "100%", maxWidth: "100%" }}>
|
||||
{getFormattedDate(item?.lastModified * 1)}
|
||||
</Paragraph>
|
||||
<Paragraph size={SIZE.xs}>
|
||||
{(item.filename || item.name).replace(".nnbackup", "")}
|
||||
</Paragraph>
|
||||
</View>
|
||||
<Button
|
||||
title="Restore"
|
||||
height={30}
|
||||
type="accent"
|
||||
style={{
|
||||
borderRadius: 100,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
fontSize={SIZE.sm - 1}
|
||||
onPress={() => restore(item, index)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View>
|
||||
<Dialog context="local" />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
paddingHorizontal: 8,
|
||||
paddingRight: 8,
|
||||
alignItems: "center",
|
||||
paddingTop: restoring ? 8 : 0
|
||||
}}
|
||||
>
|
||||
<DialogHeader
|
||||
title="Backups"
|
||||
paragraph={`All the backups are stored in ${
|
||||
Platform.OS === "ios"
|
||||
? "File Manager/Notesnook/Backups"
|
||||
: "selected backups folder."
|
||||
}`}
|
||||
button={button}
|
||||
/>
|
||||
</View>
|
||||
<Seperator half />
|
||||
<FlatList
|
||||
ListEmptyComponent={
|
||||
!restoring ? (
|
||||
loading ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 100
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator
|
||||
color={colors.primary.accent}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 100
|
||||
}}
|
||||
>
|
||||
{Platform.OS === "android" && !backupDirectoryAndroid ? (
|
||||
<>
|
||||
<Button
|
||||
title="Select backups folder"
|
||||
icon="folder"
|
||||
onPress={async () => {
|
||||
let folder = await ScopedStorage.openDocumentTree(
|
||||
true
|
||||
);
|
||||
let subfolder;
|
||||
if (folder.name !== "Notesnook backups") {
|
||||
subfolder = await ScopedStorage.createDirectory(
|
||||
folder.uri,
|
||||
"Notesnook backups"
|
||||
);
|
||||
} else {
|
||||
subfolder = folder;
|
||||
}
|
||||
SettingsService.set({
|
||||
backupDirectoryAndroid: subfolder
|
||||
});
|
||||
setBackupDirectoryAndroid(subfolder);
|
||||
setLoading(true);
|
||||
checkBackups();
|
||||
}}
|
||||
style={{
|
||||
marginTop: 10,
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
height={30}
|
||||
width={null}
|
||||
/>
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 5
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
textBreakStrategy="balanced"
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
Select the folder that includes your backup files to
|
||||
list them here.
|
||||
</Paragraph>
|
||||
</>
|
||||
) : (
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
No backups found
|
||||
</Paragraph>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: 200
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator color={colors.primary.accent} />
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
Restoring {progress ? progress?.collection : null}
|
||||
{progress ? `(${progress.current}/${progress.total}) ` : null}
|
||||
...Please wait.
|
||||
</Paragraph>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
keyExtractor={(item) => item.name || item.filename}
|
||||
style={{
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
data={restoring || loading ? [] : files}
|
||||
renderItem={renderItem}
|
||||
ListFooterComponent={
|
||||
restoring || loading || files.length === 0 ? null : (
|
||||
<View
|
||||
style={{
|
||||
height: 200
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -42,6 +42,7 @@ import { eSendEvent } from "../../services/event-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import { eClearEditor } from "../../utils/events";
|
||||
import { useSideBarDraggingStore } from "../side-menu/dragging-store";
|
||||
import { useTabStore } from "../../screens/editor/tiptap/use-tab-store";
|
||||
|
||||
interface TabProps extends ViewProps {
|
||||
dimensions: { width: number; height: number };
|
||||
@@ -85,7 +86,9 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
);
|
||||
const translateX = useSharedValue(
|
||||
widths
|
||||
? appState && !appState?.movedAway
|
||||
? appState &&
|
||||
appState?.movedAway === false &&
|
||||
useTabStore.getState().getCurrentNoteId()
|
||||
? widths.sidebar + widths.list
|
||||
: widths.sidebar
|
||||
: 0
|
||||
@@ -121,9 +124,9 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
} else {
|
||||
if (prevWidths.current?.sidebar !== widths.sidebar) {
|
||||
translateX.value =
|
||||
!appState || (appState && appState?.movedAway)
|
||||
? widths.sidebar
|
||||
: editorPosition;
|
||||
appState && appState?.movedAway === false
|
||||
? editorPosition
|
||||
: widths.sidebar;
|
||||
if (translateX.value === editorPosition) {
|
||||
onChangeTab?.({ i: 2, from: 1 });
|
||||
}
|
||||
|
||||
@@ -1,235 +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 { TouchableOpacity, View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eHideToast, eShowToast } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
export const Toast = ({ context = "global" }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [data, setData] = useState({});
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const hideTimeout = useRef();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const toastMessages = useRef([]);
|
||||
|
||||
const show = useCallback(
|
||||
async (data) => {
|
||||
if (!data) return;
|
||||
if (data.context !== context) return;
|
||||
if (
|
||||
toastMessages.current.findIndex((m) => m.message === data.message) >= 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
toastMessages.current.push(data);
|
||||
if (toastMessages.current?.length > 1) return;
|
||||
setData(data);
|
||||
|
||||
setVisible(true);
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hide();
|
||||
}, data.duration);
|
||||
},
|
||||
[context, hide]
|
||||
);
|
||||
|
||||
const next = useCallback(
|
||||
(data) => {
|
||||
if (!data) {
|
||||
hide();
|
||||
return;
|
||||
}
|
||||
setData(data);
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hide();
|
||||
}, data?.duration);
|
||||
},
|
||||
[hide]
|
||||
);
|
||||
|
||||
const hide = useCallback(() => {
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
let msg =
|
||||
toastMessages.current.length > 1 ? toastMessages.current.shift() : null;
|
||||
|
||||
if (msg) {
|
||||
setVisible(false);
|
||||
next(msg);
|
||||
setTimeout(() => {
|
||||
setVisible(true);
|
||||
}, 300);
|
||||
} else {
|
||||
setVisible(false);
|
||||
toastMessages.current.shift();
|
||||
setTimeout(() => {
|
||||
setData({});
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, [next]);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eShowToast, show);
|
||||
eSubscribeEvent(eHideToast, hide);
|
||||
return () => {
|
||||
toastMessages.current = [];
|
||||
eUnSubscribeEvent(eShowToast, show);
|
||||
eUnSubscribeEvent(eHideToast, hide);
|
||||
};
|
||||
}, [hide, show]);
|
||||
|
||||
return (
|
||||
visible && (
|
||||
<TouchableOpacity
|
||||
onPress={hide}
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
width: DDS.isTab ? 400 : "100%",
|
||||
alignItems: "center",
|
||||
alignSelf: "center",
|
||||
minHeight: 30,
|
||||
top: insets.top + 10,
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
elevation: 15
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
maxWidth: "95%",
|
||||
backgroundColor: colors.secondary.background,
|
||||
minWidth: data?.func ? "95%" : "50%",
|
||||
alignSelf: "center",
|
||||
borderRadius: 5,
|
||||
minHeight: 30,
|
||||
paddingVertical: 10,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 5,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
width: "95%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
flexGrow: 1,
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
height: 30,
|
||||
borderRadius: 100,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: 10
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={data?.type === "success" ? "check" : "close"}
|
||||
size={SIZE.lg}
|
||||
color={
|
||||
data?.type === "error"
|
||||
? colors.error.icon
|
||||
: colors.primary.accent
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
paddingRight: 25
|
||||
}}
|
||||
>
|
||||
{data?.heading ? (
|
||||
<Heading
|
||||
color={colors.primary.paragraph}
|
||||
size={SIZE.md}
|
||||
onPress={() => {
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
{data.heading}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{data?.message ? (
|
||||
<Paragraph
|
||||
color={colors.primary.paragraph}
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
paddingRight: 10
|
||||
}}
|
||||
onPress={() => {
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
{data.message}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{data.func ? (
|
||||
<Button
|
||||
testID={notesnook.toast.button}
|
||||
fontSize={SIZE.md}
|
||||
type={data.type === "error" ? "errorShade" : "transparent"}
|
||||
onPress={data.func}
|
||||
title={data.actionText}
|
||||
height={30}
|
||||
style={{
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)
|
||||
);
|
||||
};
|
||||
219
apps/mobile/app/components/toast/index.tsx
Normal file
219
apps/mobile/app/components/toast/index.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
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, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Platform,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { notesnook } from "../../../e2e/test.ids";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import useKeyboard from "../../hooks/use-keyboard";
|
||||
import { DDS } from "../../services/device-detection";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent,
|
||||
ToastOptions
|
||||
} from "../../services/event-manager";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import { eHideToast, eShowToast } from "../../utils/events";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Button } from "../ui/button";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
|
||||
export const Toast = ({ context = "global" }) => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const [toastOptions, setToastOptions] = useState<ToastOptions | undefined>();
|
||||
const hideTimeout = useRef<NodeJS.Timeout | undefined>();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const toastMessages = useRef<ToastOptions[]>([]);
|
||||
const dimensions = useWindowDimensions();
|
||||
const keyboard = useKeyboard();
|
||||
|
||||
const hideToast = useCallback(() => {
|
||||
const nextToastMessage = toastMessages.current.shift();
|
||||
if (nextToastMessage) {
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
setVisible(true);
|
||||
setToastOptions(nextToastMessage);
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hideToast();
|
||||
}, nextToastMessage?.duration);
|
||||
} else {
|
||||
setVisible(false);
|
||||
setToastOptions(undefined);
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const showToast = useCallback(
|
||||
(data?: ToastOptions) => {
|
||||
if (
|
||||
!data ||
|
||||
data.context !== context ||
|
||||
toastMessages.current.findIndex((m) => m.message === data.message) != -1
|
||||
)
|
||||
return;
|
||||
|
||||
toastMessages.current.push(data);
|
||||
if (toastMessages.current?.length > 1) return;
|
||||
|
||||
if (hideTimeout.current) {
|
||||
clearTimeout(hideTimeout.current);
|
||||
}
|
||||
setVisible(true);
|
||||
const nextToastMessage = toastMessages.current.shift();
|
||||
setToastOptions(nextToastMessage);
|
||||
hideTimeout.current = setTimeout(() => {
|
||||
hideToast();
|
||||
}, nextToastMessage?.duration);
|
||||
},
|
||||
[context, hideToast]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eShowToast, showToast);
|
||||
eSubscribeEvent(eHideToast, hideToast);
|
||||
return () => {
|
||||
eUnSubscribeEvent(eShowToast, showToast);
|
||||
eUnSubscribeEvent(eHideToast, hideToast);
|
||||
};
|
||||
}, [hideToast, showToast]);
|
||||
|
||||
const isFullToastMessage = toastOptions?.heading && toastOptions?.message;
|
||||
|
||||
return visible && toastOptions ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
hideToast();
|
||||
}}
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
width: DDS.isTab ? dimensions.width / 2 : "100%",
|
||||
alignItems: "center",
|
||||
alignSelf: "center",
|
||||
bottom:
|
||||
Platform.OS === "android"
|
||||
? Math.max(insets.bottom, 40)
|
||||
: Math.max(insets.bottom, 40) +
|
||||
(keyboard.keyboardShown ? keyboard.keyboardHeight : 0),
|
||||
position: "absolute",
|
||||
zIndex: 999,
|
||||
elevation: 15
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
...getElevationStyle(5),
|
||||
backgroundColor: isDark ? colors.static.black : colors.static.white,
|
||||
alignSelf: "center",
|
||||
borderRadius: 100,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "space-between",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
maxWidth: "95%",
|
||||
gap: 10,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name={
|
||||
toastOptions.type === "success"
|
||||
? "check"
|
||||
: toastOptions.type === "info"
|
||||
? "information"
|
||||
: "close"
|
||||
}
|
||||
size={isFullToastMessage ? SIZE.xxxl : SIZE.xl}
|
||||
color={
|
||||
toastOptions?.icon
|
||||
? toastOptions?.icon
|
||||
: toastOptions.type === "error"
|
||||
? colors.error.icon
|
||||
: toastOptions.type === "info"
|
||||
? isDark
|
||||
? colors.static.white
|
||||
: colors.static.black
|
||||
: colors.success.icon
|
||||
}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingRight: toastOptions?.func ? 0 : 12,
|
||||
flexShrink: 1
|
||||
}}
|
||||
>
|
||||
{isFullToastMessage ? (
|
||||
<Heading
|
||||
color={!isDark ? colors.static.black : colors.static.white}
|
||||
size={SIZE.sm}
|
||||
>
|
||||
{toastOptions.heading}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{toastOptions.message || toastOptions.heading ? (
|
||||
<Paragraph
|
||||
color={!isDark ? colors.static.black : colors.static.white}
|
||||
size={SIZE.xs}
|
||||
>
|
||||
{toastOptions.message || toastOptions.heading}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{toastOptions.func ? (
|
||||
<Button
|
||||
testID={notesnook.toast.button}
|
||||
fontSize={SIZE.md}
|
||||
type={toastOptions.type === "error" ? "errorShade" : "transparent"}
|
||||
onPress={toastOptions.func}
|
||||
title={toastOptions.actionText}
|
||||
height={35}
|
||||
style={{
|
||||
zIndex: 10
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
) : null;
|
||||
};
|
||||
@@ -25,7 +25,8 @@ import {
|
||||
DimensionValue,
|
||||
TextStyle,
|
||||
View,
|
||||
ViewStyle
|
||||
ViewStyle,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { useUserStore } from "../../../stores/use-user-store";
|
||||
@@ -92,6 +93,9 @@ export const Button = ({
|
||||
});
|
||||
const textColor = buttonType?.text ? buttonType.text : text;
|
||||
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const growFactor = 1 + (fontScale - 1) / 10;
|
||||
|
||||
const Component = bold ? Heading : Paragraph;
|
||||
|
||||
return (
|
||||
@@ -108,7 +112,7 @@ export const Button = ({
|
||||
NativeTooltip.show(event, tooltipText, NativeTooltip.POSITIONS.TOP);
|
||||
}
|
||||
}}
|
||||
disabled={loading}
|
||||
disabled={loading || restProps.disabled}
|
||||
type={type}
|
||||
accentColor={accentColor}
|
||||
accentText={accentText}
|
||||
@@ -117,14 +121,18 @@ export const Button = ({
|
||||
customOpacity={buttonType?.opacity}
|
||||
customAlpha={buttonType?.alpha}
|
||||
style={{
|
||||
height: height,
|
||||
width: (width as DimensionValue) || undefined,
|
||||
height: typeof height === "number" ? height * growFactor : height,
|
||||
width:
|
||||
typeof width === "number"
|
||||
? width * growFactor
|
||||
: (width as DimensionValue) || undefined,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 5,
|
||||
alignSelf: "center",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
opacity: restProps?.disabled ? 0.5 : 1,
|
||||
...(style as ViewStyle)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -41,6 +41,7 @@ import { SIZE } from "../../../utils/size";
|
||||
import { IconButton } from "../icon-button";
|
||||
import Paragraph from "../typography/paragraph";
|
||||
import phone from "phone";
|
||||
import isURL from "validator/lib/isURL";
|
||||
|
||||
interface InputProps extends TextInputProps {
|
||||
fwdRef?: RefObject<TextInput>;
|
||||
@@ -49,7 +50,8 @@ interface InputProps extends TextInputProps {
|
||||
| "email"
|
||||
| "confirmPassword"
|
||||
| "username"
|
||||
| "phonenumber";
|
||||
| "phonenumber"
|
||||
| "url";
|
||||
loading?: boolean;
|
||||
onSubmit?: (
|
||||
event: NativeSyntheticEvent<TextInputSubmitEditingEventData>
|
||||
@@ -151,6 +153,9 @@ const Input = ({
|
||||
case "confirmPassword":
|
||||
isError = customValidator && value === customValidator();
|
||||
break;
|
||||
case "url":
|
||||
isError = isURL(value);
|
||||
break;
|
||||
case "phonenumber": {
|
||||
const result = phone(value, {
|
||||
strictDetection: true,
|
||||
|
||||
@@ -21,11 +21,12 @@ import { VariantsWithStaticColors, useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useCallback } from "react";
|
||||
import {
|
||||
ColorValue,
|
||||
PressableStateCallbackType,
|
||||
Pressable as RNPressable,
|
||||
PressableProps as RNPressableProps,
|
||||
PressableStateCallbackType,
|
||||
View,
|
||||
ViewStyle
|
||||
ViewStyle,
|
||||
useWindowDimensions
|
||||
} from "react-native";
|
||||
import {
|
||||
RGB_Linear_Shade,
|
||||
@@ -256,6 +257,8 @@ export const Pressable = ({
|
||||
? 1
|
||||
: colorOpacity;
|
||||
const alpha = customAlpha ? customAlpha : isDark ? 0.03 : -0.03;
|
||||
const { fontScale } = useWindowDimensions();
|
||||
const growFactor = 1 + (fontScale - 1) / 8;
|
||||
|
||||
const getStyle = useCallback(
|
||||
({ pressed }: PressableStateCallbackType): ViewStyle | ViewStyle[] => [
|
||||
@@ -276,7 +279,13 @@ export const Pressable = ({
|
||||
: borderColor || "transparent",
|
||||
borderWidth: borderWidth
|
||||
},
|
||||
style
|
||||
style,
|
||||
{
|
||||
height:
|
||||
typeof style.height === "number"
|
||||
? style.height * growFactor
|
||||
: style.height
|
||||
}
|
||||
],
|
||||
[
|
||||
alpha,
|
||||
@@ -288,7 +297,8 @@ export const Pressable = ({
|
||||
borderSelectedColor,
|
||||
borderColor,
|
||||
borderWidth,
|
||||
style
|
||||
style,
|
||||
growFactor
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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 { isReminderActive } from "@notesnook/core/dist/collections/reminders";
|
||||
import { isReminderActive } from "@notesnook/core";
|
||||
import React from "react";
|
||||
import { ViewStyle } from "react-native";
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../../utils/size";
|
||||
import { Button, ButtonProps } from "../button";
|
||||
import { getFormattedReminderTime } from "@notesnook/common";
|
||||
import { Reminder } from "@notesnook/core/dist/types";
|
||||
import { Reminder } from "@notesnook/core";
|
||||
|
||||
export const ReminderTime = ({
|
||||
checkIsActive = true,
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/* eslint-disable no-inner-declarations */
|
||||
import { VAULT_ERRORS } from "@notesnook/core/dist/api/vault";
|
||||
import { VAULT_ERRORS } from "@notesnook/core";
|
||||
import {
|
||||
Color,
|
||||
ItemReference,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
Reminder,
|
||||
Tag,
|
||||
TrashItem
|
||||
} from "@notesnook/core/dist/types";
|
||||
} from "@notesnook/core";
|
||||
import { DisplayedNotification } from "@notifee/react-native";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
@@ -18,18 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { User } from "@notesnook/core";
|
||||
import {
|
||||
EV,
|
||||
EVENTS,
|
||||
SYNC_CHECK_IDS,
|
||||
SyncStatusEvent
|
||||
} from "@notesnook/core/dist/common";
|
||||
import { EventManagerSubscription } from "@notesnook/core/dist/utils/event-manager";
|
||||
import { EV, EVENTS, SYNC_CHECK_IDS, SyncStatusEvent } from "@notesnook/core";
|
||||
import { EventManagerSubscription } from "@notesnook/core";
|
||||
import notifee from "@notifee/react-native";
|
||||
import NetInfo, {
|
||||
NetInfoState,
|
||||
NetInfoSubscription
|
||||
} from "@react-native-community/netinfo";
|
||||
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
AppState,
|
||||
@@ -47,6 +39,7 @@ import { checkVersion } from "react-native-check-version";
|
||||
import Config from "react-native-config";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { DatabaseLogger, db, setupDatabase } from "../common/database";
|
||||
import { initializeLogger } from "../common/database/logger";
|
||||
import { MMKV } from "../common/database/mmkv";
|
||||
import Migrate from "../components/sheets/migrate";
|
||||
import NewFeature from "../components/sheets/new-feature";
|
||||
@@ -97,6 +90,8 @@ import {
|
||||
import { getGithubVersion } from "../utils/github-version";
|
||||
import { tabBarRef } from "../utils/global-refs";
|
||||
import { sleep } from "../utils/time";
|
||||
import { NotesnookModule } from "../utils/notesnook-module";
|
||||
import { changeSystemBarColors } from "../stores/use-theme-store";
|
||||
|
||||
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
|
||||
const { disableSync, disableAutoSync } = SettingsService.get();
|
||||
@@ -256,9 +251,9 @@ async function saveEditorState() {
|
||||
movedAway: editorState().movedAway,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
MMKV.setString("appState", state);
|
||||
NotesnookModule.setAppState(state);
|
||||
} else {
|
||||
MMKV.removeItem("appState");
|
||||
NotesnookModule.setAppState("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,8 +275,111 @@ const onSubscriptionError = async (error: RNIap.PurchaseError) => {
|
||||
|
||||
const SodiumEventEmitter = new NativeEventEmitter(NativeModules.Sodium);
|
||||
|
||||
const doAppLoadActions = async () => {
|
||||
if (SettingsService.get().sessionExpired) {
|
||||
eSendEvent(eLoginSessionExpired);
|
||||
return;
|
||||
}
|
||||
notifee.setBadgeCount(0);
|
||||
|
||||
if (!(await db.user.getUser())) {
|
||||
setLoginMessage();
|
||||
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;
|
||||
if (dialogs.length > 0) {
|
||||
eSendEvent(eOpenAnnouncementDialog, dialogs[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkAppUpdateAvailable = async () => {
|
||||
if (__DEV__ || Config.isTesting === "true" || Config.FDROID_BUILD || BETA)
|
||||
return;
|
||||
try {
|
||||
const version =
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
? await getGithubVersion()
|
||||
: await checkVersion();
|
||||
if (!version || !version?.needsUpdate) return false;
|
||||
|
||||
setUpdateAvailableMessage(version);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkForRateAppRequest = async () => {
|
||||
const rateApp = SettingsService.get().rateApp as number;
|
||||
if (
|
||||
rateApp &&
|
||||
rateApp < Date.now() &&
|
||||
!useMessageStore.getState().message?.visible
|
||||
) {
|
||||
setRateAppMessage();
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const IsDatabaseMigrationRequired = () => {
|
||||
if (!db.migrations.required() || useUserStore.getState().appLocked)
|
||||
return false;
|
||||
|
||||
presentSheet({
|
||||
component: <Migrate />,
|
||||
onClose: () => {
|
||||
if (!db.migrations.required()) {
|
||||
initializeDatabase();
|
||||
}
|
||||
},
|
||||
disableClosing: true
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
const initializeDatabase = async (password?: string) => {
|
||||
if (useUserStore.getState().appLocked) return;
|
||||
if (!db.isInitialized) {
|
||||
RNBootSplash.hide({ fade: false });
|
||||
changeSystemBarColors();
|
||||
|
||||
DatabaseLogger.info("Initializing database");
|
||||
try {
|
||||
await setupDatabase(password);
|
||||
await db.init();
|
||||
Sync.run();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
ToastManager.error(e as Error, "Error initializing database", "global");
|
||||
}
|
||||
}
|
||||
|
||||
if (IsDatabaseMigrationRequired()) return;
|
||||
|
||||
if (db.isInitialized) {
|
||||
Notifications.setupReminders(true);
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(false);
|
||||
}
|
||||
useSettingStore.getState().setAppLoading(false);
|
||||
DatabaseLogger.info("Database initialized");
|
||||
}
|
||||
Walkthrough.init();
|
||||
};
|
||||
|
||||
export const useAppEvents = () => {
|
||||
const loading = useSettingStore((state) => state.isAppLoading);
|
||||
const isAppLoading = useSettingStore((state) => state.isAppLoading);
|
||||
const [setLastSynced, setUser, appLocked, syncing] = useUserStore((state) => [
|
||||
state.setLastSynced,
|
||||
state.setUser,
|
||||
@@ -290,18 +388,15 @@ export const useAppEvents = () => {
|
||||
]);
|
||||
|
||||
const syncedOnLaunch = useRef(false);
|
||||
|
||||
const refValues = useRef<
|
||||
Partial<{
|
||||
subsriptionSuccessListener: EmitterSubscription;
|
||||
subsriptionErrorListener: EmitterSubscription;
|
||||
isUserReady: boolean;
|
||||
prevState: AppStateStatus;
|
||||
showingDialog: boolean;
|
||||
removeInternetStateListener: NetInfoSubscription;
|
||||
isReconnecting: boolean;
|
||||
initialUrl: string;
|
||||
backupDidWait: boolean;
|
||||
isConnectingSSE: boolean;
|
||||
}>
|
||||
>({});
|
||||
|
||||
@@ -312,7 +407,7 @@ export const useAppEvents = () => {
|
||||
}, [setLastSynced]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loading) return;
|
||||
if (isAppLoading) return;
|
||||
|
||||
let subscriptions: EventManagerSubscription[] = [];
|
||||
const eventManager = db.eventManager;
|
||||
@@ -327,7 +422,7 @@ export const useAppEvents = () => {
|
||||
return () => {
|
||||
subscriptions.forEach((sub) => sub?.unsubscribe?.());
|
||||
};
|
||||
}, [loading, onSyncComplete]);
|
||||
}, [isAppLoading, onSyncComplete]);
|
||||
|
||||
const subscribeToPurchaseListeners = useCallback(async () => {
|
||||
if (Platform.OS === "android") {
|
||||
@@ -364,14 +459,31 @@ export const useAppEvents = () => {
|
||||
const user = await db.user.getUser();
|
||||
if (PremiumService.get() && user) {
|
||||
if (
|
||||
await BackupService.checkBackupRequired(SettingsService.get().reminder)
|
||||
SettingsService.get().backupDirectoryAndroid ||
|
||||
Platform.OS !== "android"
|
||||
) {
|
||||
if (
|
||||
!SettingsService.get().backupDirectoryAndroid &&
|
||||
Platform.OS === "android"
|
||||
)
|
||||
return;
|
||||
sleep(2000).then(() => BackupService.run());
|
||||
const partialBackup = await BackupService.checkBackupRequired(
|
||||
SettingsService.get().reminder,
|
||||
"lastBackupDate"
|
||||
);
|
||||
const fullBackup = await BackupService.checkBackupRequired(
|
||||
SettingsService.get().fullBackupReminder,
|
||||
"lastFullBackupDate"
|
||||
);
|
||||
|
||||
await sleep(2000);
|
||||
|
||||
if (partialBackup) {
|
||||
await BackupService.run();
|
||||
}
|
||||
|
||||
if (fullBackup) {
|
||||
await BackupService.run(true, undefined, "full");
|
||||
}
|
||||
}
|
||||
|
||||
if (SettingsService.getProperty("offlineMode")) {
|
||||
db.attachments.cacheAttachments().catch(console.log);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
@@ -432,7 +544,6 @@ export const useAppEvents = () => {
|
||||
setRecoveryKeyMessage();
|
||||
}
|
||||
if (!user?.isEmailConfirmed) setEmailVerifyMessage();
|
||||
refValues.current.isUserReady = true;
|
||||
|
||||
syncedOnLaunch.current = true;
|
||||
if (!isLogin) {
|
||||
@@ -506,20 +617,21 @@ export const useAppEvents = () => {
|
||||
}, [onSyncComplete, onUserUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
const onInternetStateChanged = async (state: NetInfoState) => {
|
||||
const onInternetStateChanged = async () => {
|
||||
if (!syncedOnLaunch.current) return;
|
||||
reconnectSSE(state);
|
||||
Sync.run("global", false, "full");
|
||||
reconnectSSE();
|
||||
};
|
||||
|
||||
const onAppStateChanged = async (state: AppStateStatus) => {
|
||||
if (state === "active") {
|
||||
notifee.setBadgeCount(0);
|
||||
updateStatusBarColor();
|
||||
|
||||
checkAutoBackup();
|
||||
await reconnectSSE();
|
||||
Sync.run("global", false, "full");
|
||||
reconnectSSE();
|
||||
await checkForShareExtensionLaunchedInBackground();
|
||||
MMKV.removeItem("appState");
|
||||
NotesnookModule.setAppState("");
|
||||
let user = await db.user.getUser();
|
||||
if (user && !user?.isEmailConfirmed) {
|
||||
try {
|
||||
@@ -572,7 +684,7 @@ export const useAppEvents = () => {
|
||||
});
|
||||
}
|
||||
let sub: NativeEventSubscription;
|
||||
if (!loading && !appLocked) {
|
||||
if (!isAppLoading && !appLocked) {
|
||||
setTimeout(() => {
|
||||
sub = AppState.addEventListener("change", onAppStateChanged);
|
||||
if (
|
||||
@@ -594,7 +706,7 @@ export const useAppEvents = () => {
|
||||
sub?.remove();
|
||||
unsubscribePurchaseListeners();
|
||||
};
|
||||
}, [loading, appLocked, checkAutoBackup]);
|
||||
}, [isAppLoading, appLocked, checkAutoBackup]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!appLocked && !syncing && refValues.current.backupDidWait) {
|
||||
@@ -603,167 +715,35 @@ export const useAppEvents = () => {
|
||||
}
|
||||
}, [appLocked, syncing, checkAutoBackup]);
|
||||
|
||||
async function reconnectSSE(connection?: NetInfoState) {
|
||||
if (refValues.current?.isReconnecting || !refValues.current?.isUserReady)
|
||||
return;
|
||||
|
||||
if (useSettingStore.getState().appDidEnterBackgroundForAction) {
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
console.log("AppDidEnterForegroundAfterAction");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SettingsService.get().sessionExpired) {
|
||||
refValues.current.isReconnecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
refValues.current.isReconnecting = true;
|
||||
let connectionState = connection;
|
||||
async function reconnectSSE() {
|
||||
try {
|
||||
if (!connectionState) {
|
||||
connectionState = await NetInfo.fetch();
|
||||
}
|
||||
|
||||
const user = await db.user.getUser();
|
||||
if (
|
||||
user &&
|
||||
connectionState.isConnected &&
|
||||
connectionState.isInternetReachable
|
||||
) {
|
||||
await db.connectSSE();
|
||||
} else {
|
||||
useUserStore.getState().setSyncing(false);
|
||||
await db.syncer.stop();
|
||||
}
|
||||
refValues.current.isReconnecting = false;
|
||||
if (refValues.current?.isConnectingSSE) return;
|
||||
refValues.current.isConnectingSSE = true;
|
||||
await db.connectSSE();
|
||||
refValues.current.isConnectingSSE = false;
|
||||
} catch (e) {
|
||||
refValues.current.isReconnecting = false;
|
||||
refValues.current.isConnectingSSE = false;
|
||||
DatabaseLogger.error(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
if (!isAppLoading) {
|
||||
onUserUpdated();
|
||||
doAppLoadActions();
|
||||
}
|
||||
}, [loading, onUserUpdated]);
|
||||
|
||||
const initializeDatabase = useCallback(async (password?: string) => {
|
||||
const IsDatabaseMigrationRequired = () => {
|
||||
if (!db.migrations.required() || useUserStore.getState().appLocked)
|
||||
return false;
|
||||
|
||||
presentSheet({
|
||||
component: <Migrate />,
|
||||
onClose: () => {
|
||||
if (!db.migrations.required()) {
|
||||
initializeDatabase();
|
||||
}
|
||||
},
|
||||
disableClosing: true
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
if (useUserStore.getState().appLocked) return;
|
||||
if (!db.isInitialized) {
|
||||
RNBootSplash.hide({ fade: true });
|
||||
DatabaseLogger.info("Initializing database");
|
||||
try {
|
||||
await setupDatabase(password);
|
||||
await db.init();
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e as Error);
|
||||
ToastManager.error(e as Error, "Error initializing database", "global");
|
||||
}
|
||||
}
|
||||
|
||||
if (IsDatabaseMigrationRequired()) return;
|
||||
|
||||
if (db.isInitialized) {
|
||||
Notifications.setupReminders(true);
|
||||
if (SettingsService.get().notifNotes) {
|
||||
Notifications.pinQuickNote(false);
|
||||
}
|
||||
useSettingStore.getState().setAppLoading(false);
|
||||
DatabaseLogger.info("Database initialized");
|
||||
}
|
||||
Walkthrough.init();
|
||||
}, []);
|
||||
}, [isAppLoading, onUserUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
let sub: () => void;
|
||||
if (appLocked) {
|
||||
const sub = useUserStore.subscribe((state) => {
|
||||
if (!state.appLocked && useSettingStore.getState().isAppLoading) {
|
||||
console.log("DB initialized");
|
||||
if (!appLocked && isAppLoading) {
|
||||
initializeLogger()
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
})
|
||||
.finally(() => {
|
||||
//@ts-ignore
|
||||
initializeDatabase();
|
||||
sub();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return () => {
|
||||
sub?.();
|
||||
};
|
||||
}, [appLocked, initializeDatabase]);
|
||||
|
||||
return initializeDatabase;
|
||||
};
|
||||
|
||||
const doAppLoadActions = async () => {
|
||||
if (SettingsService.get().sessionExpired) {
|
||||
eSendEvent(eLoginSessionExpired);
|
||||
return;
|
||||
}
|
||||
notifee.setBadgeCount(0);
|
||||
|
||||
if (!(await db.user.getUser())) {
|
||||
setLoginMessage();
|
||||
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;
|
||||
if (dialogs.length > 0) {
|
||||
eSendEvent(eOpenAnnouncementDialog, dialogs[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const checkAppUpdateAvailable = async () => {
|
||||
if (__DEV__ || Config.isTesting === "true" || Config.FDROID_BUILD || BETA)
|
||||
return;
|
||||
try {
|
||||
const version =
|
||||
Config.GITHUB_RELEASE === "true"
|
||||
? await getGithubVersion()
|
||||
: await checkVersion();
|
||||
if (!version || !version?.needsUpdate) return false;
|
||||
|
||||
setUpdateAvailableMessage(version);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkForRateAppRequest = async () => {
|
||||
const rateApp = SettingsService.get().rateApp as number;
|
||||
if (
|
||||
rateApp &&
|
||||
rateApp < Date.now() &&
|
||||
!useMessageStore.getState().message?.visible
|
||||
) {
|
||||
setRateAppMessage();
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}, [appLocked, isAppLoading]);
|
||||
};
|
||||
|
||||
817
apps/mobile/app/hooks/use-pricing-plans.ts
Normal file
817
apps/mobile/app/hooks/use-pricing-plans.ts
Normal file
@@ -0,0 +1,817 @@
|
||||
/*
|
||||
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 { useEffect, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import * as RNIap from "react-native-iap";
|
||||
import { DatabaseLogger, db } from "../common/database";
|
||||
import { eSendEvent } from "../services/event-manager";
|
||||
import PremiumService from "../services/premium";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import { useUserStore } from "../stores/use-user-store";
|
||||
import { eClosePremiumDialog, eCloseSheet } from "../utils/events";
|
||||
import { sleep } from "../utils/time";
|
||||
|
||||
function numberWithCommas(x: string) {
|
||||
const parts = x.toString().split(".");
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
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",
|
||||
// no trial
|
||||
"notesnook.essential.monthly.nt",
|
||||
"notesnook.essential.yearly.nt"
|
||||
],
|
||||
trialSupported: true,
|
||||
productSkuList: []
|
||||
},
|
||||
{
|
||||
id: "pro",
|
||||
name: "Pro",
|
||||
description: "Unlocks all features for professional use",
|
||||
subscriptionSkuList: [
|
||||
"notesnook.pro.monthly",
|
||||
"notesnook.pro.yearly",
|
||||
"notesnook.pro.monthly.tier2",
|
||||
"notesnook.pro.yearly.tier2",
|
||||
"notesnook.pro.monthly.tier3",
|
||||
"notesnook.pro.yearly.tier3",
|
||||
// no trial
|
||||
"notesnook.pro.monthly.nt",
|
||||
"notesnook.pro.yearly.nt",
|
||||
"notesnook.pro.monthly.tier2.nt",
|
||||
"notesnook.pro.yearly.tier2.nt",
|
||||
"notesnook.pro.monthly.tier3.nt",
|
||||
"notesnook.pro.yearly.tier3.nt"
|
||||
],
|
||||
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",
|
||||
// no trial
|
||||
"notesnook.believer.monthly.nt",
|
||||
"notesnook.believer.yearly.nt"
|
||||
],
|
||||
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 usePricingPlans = (options?: PricingPlansOptions) => {
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [currentPlan, setCurrentPlan] = useState<string>(
|
||||
options?.planId || pricingPlans[0].id
|
||||
);
|
||||
const [plans, setPlans] = useState<PricingPlan[]>(pricingPlans);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingPlans, setLoadingPlans] = useState(true);
|
||||
const [selectedProductSku, setSelectedProductSku] = useState<
|
||||
string | undefined
|
||||
>(options?.productId || undefined);
|
||||
const [isPromoOffer, setIsPromoOffer] = useState(false);
|
||||
const [cancelPromo, setCancelPromo] = useState(false);
|
||||
|
||||
const getProduct = (planId: string, skuId: string) => {
|
||||
return (
|
||||
plans.find((p) => p.id === planId)?.subscriptions?.[skuId] ||
|
||||
plans.find((p) => p.id === planId)?.products?.[skuId]
|
||||
);
|
||||
};
|
||||
|
||||
const getProductAndroid = (planId: string, skuId: string) => {
|
||||
return getProduct(planId, skuId) as RNIap.SubscriptionAndroid;
|
||||
};
|
||||
|
||||
const getProductIOS = (planId: string, skuId: string) => {
|
||||
return getProduct(planId, skuId) as RNIap.SubscriptionIOS;
|
||||
};
|
||||
|
||||
const hasTrialOffer = () => {
|
||||
if (!selectedProductSku) return false;
|
||||
|
||||
return Platform.OS === "ios"
|
||||
? !!(getProduct(currentPlan, selectedProductSku) as RNIap.SubscriptionIOS)
|
||||
?.introductoryPrice
|
||||
: (
|
||||
getProduct(
|
||||
currentPlan,
|
||||
selectedProductSku
|
||||
) as RNIap.SubscriptionAndroid
|
||||
).subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList
|
||||
?.length > 1;
|
||||
};
|
||||
|
||||
const userCanRequestTrial = hasTrialOffer();
|
||||
// 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]);
|
||||
setLoadingPlans(false);
|
||||
};
|
||||
|
||||
const loadPromoOffer = async () => {
|
||||
if (cancelPromo) {
|
||||
setIsPromoOffer(false);
|
||||
return;
|
||||
}
|
||||
if (options?.promoOffer?.promoCode) {
|
||||
const promoCode = options.promoOffer.promoCode;
|
||||
let skuId: string;
|
||||
if (promoCode.startsWith("com.streetwriters.notesnook")) {
|
||||
skuId = promoCode;
|
||||
} else {
|
||||
skuId = await db.offers?.getCode(
|
||||
promoCode.split(":")[0],
|
||||
Platform.OS as "ios" | "android" | "web"
|
||||
);
|
||||
}
|
||||
|
||||
const plan = pricingPlans.find((p) =>
|
||||
p.subscriptionSkuList.includes(skuId)
|
||||
);
|
||||
if (plan) {
|
||||
setCurrentPlan(plan.id);
|
||||
setSelectedProductSku(skuId);
|
||||
setIsPromoOffer(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadPlans().then(() => loadPromoOffer());
|
||||
}, [options?.promoOffer, cancelPromo]);
|
||||
|
||||
function getLocalizedPrice(product: RNIap.Subscription | RNIap.Product) {
|
||||
if (!product) return;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
const pricingPhaseListItem = (product as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails?.[0]?.pricingPhases?.pricingPhaseList?.[1];
|
||||
|
||||
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,
|
||||
androidOfferToken?: string
|
||||
) {
|
||||
if (loading || !product) 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,
|
||||
/**
|
||||
* iOS
|
||||
*/
|
||||
appAccountToken: toUUID(user.id),
|
||||
andDangerouslyFinishTransactionAutomaticallyIOS: false,
|
||||
subscriptionOffers: androidOfferToken
|
||||
? [
|
||||
{
|
||||
offerToken: androidOfferToken,
|
||||
sku: product?.productId
|
||||
}
|
||||
]
|
||||
: undefined
|
||||
});
|
||||
} else {
|
||||
await RNIap.requestPurchase({
|
||||
andDangerouslyFinishTransactionAutomaticallyIOS: false,
|
||||
appAccountToken: toUUID(user.id),
|
||||
obfuscatedAccountIdAndroid: user.id,
|
||||
obfuscatedProfileIdAndroid: user.id,
|
||||
sku: product.productId,
|
||||
quantity: 1
|
||||
});
|
||||
}
|
||||
useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
setLoading(false);
|
||||
options?.onBuy?.();
|
||||
} catch (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,
|
||||
offerIndex: number
|
||||
) => {
|
||||
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,
|
||||
offerIndex: number,
|
||||
phaseIndex: number,
|
||||
trialDurationIos?: boolean
|
||||
) => {
|
||||
if (product.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];
|
||||
const androidPricingPhase2 = (p2 as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails?.[0].pricingPhases?.pricingPhaseList?.[1];
|
||||
|
||||
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,
|
||||
phaseIndex: number,
|
||||
annualBilling?: boolean
|
||||
) => {
|
||||
if (!product) return null;
|
||||
|
||||
const androidPricingPhase = (product as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails?.[0].pricingPhases?.pricingPhaseList?.[
|
||||
phaseIndex
|
||||
];
|
||||
console.log(product as RNIap.Product);
|
||||
|
||||
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?.productId.includes("5year")
|
||||
? getLocalizedPrice(product as RNIap.Subscription)
|
||||
: convertPrice(
|
||||
priceValue,
|
||||
priceSymbol,
|
||||
localizedPrice.startsWith(priceSymbol),
|
||||
annualBilling ? 12 : 60
|
||||
);
|
||||
};
|
||||
|
||||
// const getPricingPhases = (product: RNIap.Subscription) => {
|
||||
// if (!product) return null;
|
||||
|
||||
// if (Platform.OS === "android") {
|
||||
// const offer = (product as RNIap.SubscriptionAndroid)
|
||||
// ?.subscriptionOfferDetails?.[0];
|
||||
|
||||
// return offer.pricingPhases?.pricingPhaseList?.map((phase, index) => {
|
||||
|
||||
// return {
|
||||
// localizedPrice: phase.formattedPrice,
|
||||
// price: parseInt(phase.priceAmountMicros) / 1000000,
|
||||
// monthlyFormattedPrice: getPrice(product, index, offer?.pricingPhases?.pricingPhaseList?.[1].billingPeriod.endsWith("Y"),
|
||||
// }
|
||||
// })
|
||||
// } else {
|
||||
// return (product as RNIap.SubscriptionIOS)?.pricingPhases;
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
return {
|
||||
currentPlan: pricingPlans.find((p) => p.id === currentPlan),
|
||||
pricingPlans: plans,
|
||||
getStandardPrice: getLocalizedPrice,
|
||||
loadingPlans,
|
||||
loading,
|
||||
selectPlan: (planId: string) => {
|
||||
setCurrentPlan(planId);
|
||||
setSelectedProductSku(
|
||||
plans.find((p) => p.id === planId)?.subscriptionSkuList?.[0]
|
||||
);
|
||||
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`
|
||||
];
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// const usePricingPlans = (options?: {
|
||||
// promo?: {
|
||||
// promoCode?: string;
|
||||
// };
|
||||
// }) => {
|
||||
// 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 yearlyPlan = usePricing("yearly");
|
||||
// const monthlyPlan = usePricing("monthly");
|
||||
// const promoCode = options?.promo?.promoCode;
|
||||
|
||||
// const getSkus = useCallback(async () => {
|
||||
// try {
|
||||
// setLoading(true);
|
||||
// if (promoCode) {
|
||||
// getPromo(promoCode);
|
||||
// }
|
||||
// setLoading(false);
|
||||
// } catch (e) {
|
||||
// setLoading(false);
|
||||
// console.log("error getting sku", e);
|
||||
// }
|
||||
// }, [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" | "web"
|
||||
// );
|
||||
// }
|
||||
|
||||
// const products = await PremiumService.getProducts();
|
||||
// const product = products.find((p) => p.productId === skuId);
|
||||
// if (!product) return false;
|
||||
// const isMonthly = product.productId.indexOf(".mo") > -1;
|
||||
|
||||
// const cycleText = isMonthly
|
||||
// ? promoCyclesMonthly[
|
||||
// (Platform.OS === "android"
|
||||
// ? (product as RNIap.SubscriptionAndroid)
|
||||
// .subscriptionOfferDetails[0]?.pricingPhases
|
||||
// .pricingPhaseList?.[0].billingCycleCount
|
||||
// : parseInt(
|
||||
// (product as RNIap.SubscriptionIOS)
|
||||
// .introductoryPriceNumberOfPeriodsIOS as string
|
||||
// )) as keyof typeof promoCyclesMonthly
|
||||
// ]
|
||||
// : promoCyclesYearly[
|
||||
// (Platform.OS === "android"
|
||||
// ? (product as RNIap.SubscriptionAndroid)
|
||||
// .subscriptionOfferDetails[0]?.pricingPhases
|
||||
// .pricingPhaseList?.[0].billingCycleCount
|
||||
// : parseInt(
|
||||
// (product as RNIap.SubscriptionIOS)
|
||||
// .introductoryPriceNumberOfPeriodsIOS as string
|
||||
// )) as keyof typeof promoCyclesYearly
|
||||
// ];
|
||||
|
||||
// setProduct({
|
||||
// type: "promo",
|
||||
// offerType: isMonthly ? "monthly" : "yearly",
|
||||
// data: product,
|
||||
// cycleText: cycleText,
|
||||
// info: `Pay ${isMonthly ? "monthly" : "yearly"}, cancel anytime`
|
||||
// });
|
||||
// return true;
|
||||
// } catch (e) {
|
||||
// console.log("PROMOCODE ERROR:", code, e);
|
||||
// return false;
|
||||
// }
|
||||
// };
|
||||
|
||||
// useEffect(() => {
|
||||
// getSkus();
|
||||
// }, [getSkus]);
|
||||
|
||||
// const buySubscription = async (product: RNIap.Subscription) => {
|
||||
// if (buying || !product) return;
|
||||
// setBuying(true);
|
||||
// try {
|
||||
// if (!user) {
|
||||
// setBuying(false);
|
||||
// return;
|
||||
// }
|
||||
// useSettingStore.getState().setAppDidEnterBackgroundForAction(true);
|
||||
// const androidOfferToken =
|
||||
// Platform.OS === "android"
|
||||
// ? (product as RNIap.SubscriptionAndroid).subscriptionOfferDetails[0]
|
||||
// .offerToken
|
||||
// : null;
|
||||
|
||||
// DatabaseLogger.info(
|
||||
// `Subscription Requested initiated for user ${toUUID(user.id)}`
|
||||
// );
|
||||
|
||||
// await RNIap.requestSubscription({
|
||||
// sku: product?.productId,
|
||||
// obfuscatedAccountIdAndroid: user.id,
|
||||
// obfuscatedProfileIdAndroid: user.id,
|
||||
// appAccountToken: toUUID(user.id),
|
||||
// andDangerouslyFinishTransactionAutomaticallyIOS: false,
|
||||
// subscriptionOffers: androidOfferToken
|
||||
// ? [
|
||||
// {
|
||||
// offerToken: androidOfferToken,
|
||||
// sku: product?.productId
|
||||
// }
|
||||
// ]
|
||||
// : undefined
|
||||
// });
|
||||
// useSettingStore.getState().setAppDidEnterBackgroundForAction(false);
|
||||
// setBuying(false);
|
||||
// eSendEvent(eCloseSheet);
|
||||
// eSendEvent(eClosePremiumDialog);
|
||||
// await sleep(500);
|
||||
// presentSheet({
|
||||
// title: "Thank you for subscribing!",
|
||||
// paragraph:
|
||||
// "Your Notesnook Pro subscription will be activated soon. If your account is not upgraded to Notesnook Pro, your money will be refunded to you. In case of any issues, please reach out to us at support@streetwriters.co",
|
||||
// action: async () => {
|
||||
// eSendEvent(eCloseSheet);
|
||||
// },
|
||||
// icon: "check",
|
||||
// actionText: "Continue"
|
||||
// });
|
||||
// } catch (e) {
|
||||
// setBuying(false);
|
||||
// console.log(e);
|
||||
// }
|
||||
// };
|
||||
|
||||
// function getStandardPrice(
|
||||
// type: "monthly" | "yearly",
|
||||
// product: RNIap.Subscription
|
||||
// ) {
|
||||
// if (!product) return;
|
||||
// const productType = type;
|
||||
|
||||
// if (Platform.OS === "android") {
|
||||
// const pricingPhaseListItem = (product 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 as RNIap.SubscriptionIOS)?.localizedPrice ||
|
||||
// (productDefault as RNIap.SubscriptionIOS)?.localizedPrice
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// return {
|
||||
// product,
|
||||
// buySubscription,
|
||||
// getStandardPrice,
|
||||
// loading,
|
||||
// userCanRequestTrial,
|
||||
// buying,
|
||||
// setBuying,
|
||||
// setLoading,
|
||||
// user,
|
||||
// setProduct,
|
||||
// monthlyPlan,
|
||||
// yearlyPlan,
|
||||
// getPromo
|
||||
// };
|
||||
// };
|
||||
|
||||
export default usePricingPlans;
|
||||
@@ -22,7 +22,7 @@ import { Platform } from "react-native";
|
||||
import { Subscription } from "react-native-iap";
|
||||
import PremiumService from "../services/premium";
|
||||
import { db } from "../common/database";
|
||||
import { Product } from "@notesnook/core/dist/api/pricing";
|
||||
import { Product } from "@notesnook/core";
|
||||
|
||||
const skuInfos: { [name: string]: Product | undefined } = {};
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { EVENTS } from "@notesnook/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { db } from "../common/database";
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
} from "@sayem314/react-native-keep-awake";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Dimensions, Platform, StatusBar, View } from "react-native";
|
||||
import changeNavigationBarColor from "react-native-navigation-bar-color";
|
||||
import {
|
||||
addOrientationListener,
|
||||
addSpecificOrientationListener,
|
||||
@@ -62,7 +61,6 @@ import {
|
||||
} from "../services/event-manager";
|
||||
import { useSettingStore } from "../stores/use-setting-store";
|
||||
import {
|
||||
eClearEditor,
|
||||
eCloseFullscreenEditor,
|
||||
eOnEnterEditor,
|
||||
eOnExitEditor,
|
||||
@@ -75,7 +73,7 @@ import { sleep } from "../utils/time";
|
||||
import { NavigationStack } from "./navigation-stack";
|
||||
|
||||
const _TabsHolder = () => {
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const { colors } = useThemeColors();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const setFullscreen = useSettingStore((state) => state.setFullscreen);
|
||||
const fullscreen = useSettingStore((state) => state.fullscreen);
|
||||
@@ -271,32 +269,29 @@ const _TabsHolder = () => {
|
||||
}
|
||||
|
||||
const state = getAppState();
|
||||
|
||||
setTimeout(() => {
|
||||
switch (current) {
|
||||
case "tablet":
|
||||
tabBarRef.current?.goToIndex(0, false);
|
||||
break;
|
||||
case "smallTablet":
|
||||
if (!fullscreen) {
|
||||
tabBarRef.current?.closeDrawer(false);
|
||||
} else {
|
||||
tabBarRef.current?.openDrawer(false);
|
||||
}
|
||||
break;
|
||||
case "mobile":
|
||||
if (
|
||||
state &&
|
||||
!state?.movedAway &&
|
||||
useTabStore.getState().getCurrentNoteId()
|
||||
) {
|
||||
tabBarRef.current?.goToIndex(2, false);
|
||||
} else {
|
||||
tabBarRef.current?.goToIndex(1, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}, 1000);
|
||||
switch (current) {
|
||||
case "tablet":
|
||||
tabBarRef.current?.goToIndex(0, false);
|
||||
break;
|
||||
case "smallTablet":
|
||||
if (!fullscreen) {
|
||||
tabBarRef.current?.closeDrawer(false);
|
||||
} else {
|
||||
tabBarRef.current?.openDrawer(false);
|
||||
}
|
||||
break;
|
||||
case "mobile":
|
||||
if (
|
||||
state &&
|
||||
editorState().movedAway === false &&
|
||||
useTabStore.getState().getCurrentNoteId()
|
||||
) {
|
||||
tabBarRef.current?.goToIndex(2, false);
|
||||
} else {
|
||||
tabBarRef.current?.goToIndex(1, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const onScroll = (scrollOffset) => {
|
||||
@@ -386,18 +381,12 @@ const _TabsHolder = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
function updateSystemBars() {
|
||||
changeNavigationBarColor(colors.primary.background, isDark, true);
|
||||
StatusBar.setBackgroundColor("transparent");
|
||||
StatusBar.setTranslucent(true);
|
||||
StatusBar.setBarStyle(isDark ? "light-content" : "dark-content");
|
||||
}
|
||||
updateSystemBars();
|
||||
setTimeout(() => {
|
||||
updateSystemBars();
|
||||
}, 1000);
|
||||
}, [colors.primary.background, isDark]);
|
||||
// useEffect(() => {
|
||||
// changeSystemBarColors();
|
||||
// setTimeout(() => {
|
||||
// changeSystemBarColors();
|
||||
// }, 1000);
|
||||
// }, [colors.primary.background, isDark]);
|
||||
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"html-to-text": "9.0.5",
|
||||
"phone": "^3.1.14",
|
||||
"qclone": "^1.2.0",
|
||||
"react-native-actions-sheet": "0.9.3",
|
||||
"react-native-actions-sheet": "0.9.7",
|
||||
"react-native-check-version": "https://github.com/flexible-agency/react-native-check-version",
|
||||
"react-native-drax": "^0.10.2",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
@@ -36,12 +36,13 @@
|
||||
"react-native-wheel-color-picker": "^1.3.1",
|
||||
"tinycolor2": "1.6.0",
|
||||
"@azure/core-asynciterator-polyfill": "^1.0.2",
|
||||
"@tanstack/react-query": "^4.29.19",
|
||||
"@trpc/client": "10.38.3",
|
||||
"@trpc/react-query": "10.38.3",
|
||||
"@trpc/server": "10.38.3",
|
||||
"kysely": "^0.26.3",
|
||||
"pathe": "1.1.2"
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@trpc/client": "10.45.2",
|
||||
"@trpc/react-query": "10.45.2",
|
||||
"@trpc/server": "10.45.2",
|
||||
"@streetwriters/kysely": "^0.27.4",
|
||||
"pathe": "1.1.2",
|
||||
"react-native-format-currency": "0.0.5"
|
||||
},
|
||||
"sideEffects": false
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ 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 { Note } from "@notesnook/core/dist/types";
|
||||
import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index";
|
||||
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
|
||||
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
|
||||
import { Note } from "@notesnook/core";
|
||||
import type { Attachment } from "@notesnook/editor";
|
||||
import type { ImageAttributes } from "@notesnook/editor";
|
||||
import type { LinkAttributes } from "@notesnook/editor";
|
||||
import { createRef, RefObject } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { EdgeInsets } from "react-native-safe-area-context";
|
||||
|
||||
@@ -18,7 +18,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { isImage } from "@notesnook/core/dist/utils/filename";
|
||||
import { isImage } from "@notesnook/core";
|
||||
import { basename } from "pathe";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import DocumentPicker, {
|
||||
@@ -41,7 +42,6 @@ import { FILE_SIZE_LIMIT, IMAGE_SIZE_LIMIT } from "../../../utils/constants";
|
||||
import { eCloseSheet } from "../../../utils/events";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { editorController, editorState } from "./utils";
|
||||
import { basename } from "pathe";
|
||||
|
||||
const showEncryptionSheet = (file: DocumentPickerResponse) => {
|
||||
presentSheet({
|
||||
@@ -123,13 +123,13 @@ const file = async (fileOptions: PickerOptions) => {
|
||||
file.name,
|
||||
fileOptions
|
||||
))
|
||||
)
|
||||
return;
|
||||
) {
|
||||
throw new Error("Failed to attach file");
|
||||
}
|
||||
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
|
||||
|
||||
if (!fileOptions.tabId) return;
|
||||
if (
|
||||
fileOptions.tabId &&
|
||||
fileOptions.tabId !== undefined &&
|
||||
fileOptions.noteId &&
|
||||
useTabStore.getState().getNoteIdForTab(fileOptions.tabId) ===
|
||||
fileOptions.noteId
|
||||
@@ -158,16 +158,16 @@ const file = async (fileOptions: PickerOptions) => {
|
||||
fileOptions.tabId
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Failed to attach file, no tabId is set");
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
eSendEvent(eCloseSheet);
|
||||
}, 1000);
|
||||
eSendEvent(eCloseSheet);
|
||||
} catch (e) {
|
||||
eSendEvent(eCloseSheet);
|
||||
ToastManager.show({
|
||||
heading: (e as Error).message,
|
||||
message: "You need internet access to attach a file",
|
||||
heading: "Failed to attach file",
|
||||
message: (e as Error).message,
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
@@ -185,7 +185,8 @@ const camera = async (options: PickerOptions) => {
|
||||
cropping: false,
|
||||
multiple: true,
|
||||
maxFiles: 10,
|
||||
writeTempFile: true
|
||||
writeTempFile: true,
|
||||
compressImageQuality: 1
|
||||
})
|
||||
.then((response) => {
|
||||
handleImageResponse(
|
||||
@@ -202,7 +203,6 @@ const camera = async (options: PickerOptions) => {
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
console.log("attachment error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -215,7 +215,8 @@ const gallery = async (options: PickerOptions) => {
|
||||
mediaType: "photo",
|
||||
maxFiles: 10,
|
||||
cropping: false,
|
||||
multiple: true
|
||||
multiple: true,
|
||||
compressImageQuality: 1
|
||||
})
|
||||
.then((response) =>
|
||||
handleImageResponse(
|
||||
@@ -266,7 +267,6 @@ const handleImageResponse = async (
|
||||
response: Image[],
|
||||
options: PickerOptions
|
||||
) => {
|
||||
console.log(response, "result-file-picker");
|
||||
const result = await AttachImage.present(response, options.context);
|
||||
if (!result) return;
|
||||
const compress = result.compress;
|
||||
@@ -302,10 +302,15 @@ const handleImageResponse = async (
|
||||
type: "url"
|
||||
});
|
||||
|
||||
const fileName = image.sourceURL
|
||||
let fileName = image.sourceURL
|
||||
? basename(image.sourceURL)
|
||||
: image.filename || "image";
|
||||
|
||||
fileName =
|
||||
image.mime === "image/jpeg"
|
||||
? fileName.replace(/HEIC|HEIF/, "jpeg")
|
||||
: fileName;
|
||||
|
||||
console.log("attaching image...", fileName);
|
||||
|
||||
console.log("attaching file...");
|
||||
|
||||
@@ -17,9 +17,8 @@ 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 type { ToolbarGroupDefinition } from "@notesnook/editor/dist/toolbar/types";
|
||||
import type { ToolbarGroupDefinition } from "@notesnook/editor";
|
||||
import { useEditor } from "./use-editor";
|
||||
import { Note } from "@notesnook/core/dist/types";
|
||||
export type useEditorType = ReturnType<typeof useEditor>;
|
||||
|
||||
export type EditorState = {
|
||||
@@ -91,7 +90,6 @@ export type SavePayload = {
|
||||
};
|
||||
|
||||
export type AppState = {
|
||||
note?: Note;
|
||||
editing: boolean;
|
||||
movedAway: boolean;
|
||||
timestamp: number;
|
||||
|
||||
@@ -19,8 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/* eslint-disable no-case-declarations */
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import { ItemReference } from "@notesnook/core/dist/types";
|
||||
import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index";
|
||||
import { ItemReference } from "@notesnook/core";
|
||||
import type { Attachment } from "@notesnook/editor";
|
||||
import { getDefaultPresets } from "@notesnook/editor/dist/toolbar/tool-definitions";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user