mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-30 18:48:27 +02:00
Compare commits
1 Commits
feat/prici
...
fdroid_rel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bde6ce190 |
@@ -15,7 +15,6 @@ const SCOPES = [
|
||||
"mobile",
|
||||
"web",
|
||||
"vericrypt",
|
||||
"monograph",
|
||||
"desktop",
|
||||
"crypto",
|
||||
"editor",
|
||||
|
||||
83
.github/workflows/monograph.publish.yml
vendored
83
.github/workflows/monograph.publish.yml
vendored
@@ -1,83 +0,0 @@
|
||||
# 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
|
||||
2410
apps/desktop/package-lock.json
generated
2410
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.18",
|
||||
"version": "3.0.13",
|
||||
"appAppleId": "1544027013",
|
||||
"private": true,
|
||||
"main": "./dist/index.js",
|
||||
@@ -11,29 +11,30 @@
|
||||
"repository": "https://github.com/streetwriters/notesnook",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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.2.1",
|
||||
"icojs": "^0.17.1",
|
||||
"sodium-native": "^4.1.1",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"yargs": "^17.7.2",
|
||||
"zod": "^3.23.8"
|
||||
"yargs": "^17.6.2",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@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.23.0",
|
||||
"kysely": "^0.27.3",
|
||||
"node-gyp-build": "^4.8.0",
|
||||
"prebuildify": "^6.0.1",
|
||||
"tree-kill": "^1.2.2",
|
||||
"undici": "^6.19.8"
|
||||
"undici": "^6.14.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"dmg-license": "^1.0.11"
|
||||
@@ -158,8 +159,7 @@
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": true,
|
||||
"createDesktopShortcut": "always",
|
||||
"deleteAppDataOnUninstall": true
|
||||
"createDesktopShortcut": "always"
|
||||
},
|
||||
"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 739a48c..7a7b4af 100644
|
||||
index fcb7f54..cb1c7f7 100644
|
||||
--- a/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
+++ b/node_modules/app-builder-lib/out/targets/LinuxTargetHelper.js
|
||||
@@ -117,6 +117,7 @@ class LinuxTargetHelper {
|
||||
@@ -116,6 +116,7 @@ class LinuxTargetHelper {
|
||||
StartupWMClass: appInfo.productName,
|
||||
...extra,
|
||||
...targetSpecificOptions.desktop,
|
||||
@@ -10,7 +10,7 @@ index 739a48c..7a7b4af 100644
|
||||
};
|
||||
const description = this.getDescription(targetSpecificOptions);
|
||||
if (!(0, builder_util_1.isEmptyOrSpaces)(description)) {
|
||||
@@ -160,6 +161,23 @@ class LinuxTargetHelper {
|
||||
@@ -159,6 +160,23 @@ class LinuxTargetHelper {
|
||||
data += `\n${name}=${desktopMeta[name]}`;
|
||||
}
|
||||
data += "\n";
|
||||
23
apps/desktop/patches/electron-trpc+0.5.2.patch
Normal file
23
apps/desktop/patches/electron-trpc+0.5.2.patch
Normal file
@@ -0,0 +1,23 @@
|
||||
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) {
|
||||
@@ -1,39 +0,0 @@
|
||||
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 = ({
|
||||
46
apps/desktop/patches/node-gyp-build+4.8.0.patch
Normal file
46
apps/desktop/patches/node-gyp-build+4.8.0.patch
Normal file
@@ -0,0 +1,46 @@
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ async function patchBetterSQLite3() {
|
||||
);
|
||||
const json = JSON.parse(await readFile(jsonPath, "utf-8"));
|
||||
|
||||
json.version = "11.2.2";
|
||||
json.version = "9.5.1";
|
||||
json.homepage = "https://github.com/thecodrr/better-sqlite3-multiple-ciphers";
|
||||
json.repository.url =
|
||||
"git://github.com/thecodrr/better-sqlite3-multiple-ciphers.git";
|
||||
|
||||
@@ -18,7 +18,7 @@ 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";
|
||||
import type { QueryResult } from "kysely";
|
||||
|
||||
type SQLiteCompatibleType =
|
||||
| number
|
||||
|
||||
@@ -29,37 +29,19 @@ 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;
|
||||
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);
|
||||
});
|
||||
cancellationToken = new CancellationToken();
|
||||
await autoUpdater
|
||||
.downloadUpdate(cancellationToken)
|
||||
.finally(() => (cancellationToken = undefined));
|
||||
}),
|
||||
check: t.procedure.query(async () => {
|
||||
if (cancellationToken) return;
|
||||
clearTimeout(downloadTimeout);
|
||||
await new Promise<void>((resolve) => {
|
||||
downloadTimeout = setTimeout(async () => {
|
||||
await autoUpdater
|
||||
.checkForUpdates()
|
||||
.catch(console.error)
|
||||
.finally(resolve);
|
||||
}, 1000);
|
||||
});
|
||||
await autoUpdater.checkForUpdates().catch(console.error);
|
||||
}),
|
||||
|
||||
toggleAutoUpdates: t.procedure
|
||||
|
||||
@@ -20,14 +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";
|
||||
|
||||
declare global {
|
||||
var os: () => "mas" | ReturnType<typeof platform>;
|
||||
var electronTRPC: RendererGlobalElectronTRPC;
|
||||
// var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
var NativeNNCrypto: (new () => NNCrypto) | undefined;
|
||||
}
|
||||
|
||||
process.once("loaded", async () => {
|
||||
@@ -40,5 +40,5 @@ process.once("loaded", async () => {
|
||||
globalThis.electronTRPC = electronTRPC;
|
||||
});
|
||||
|
||||
// globalThis.NativeNNCrypto = require("@notesnook/crypto").NNCrypto;
|
||||
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 { ParsedImage, parseICO } from "icojs";
|
||||
import { parse, ParsedImage } 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 parseICO(icoBuffer, "image/png");
|
||||
const images = await parse(icoBuffer, "image/png");
|
||||
ALL_ICONS.push({ id: icon, images, prefix });
|
||||
}
|
||||
}
|
||||
|
||||
156
apps/mobile/app/common/analytics/index.js
Normal file
156
apps/mobile/app/common/analytics/index.js
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
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
|
||||
};
|
||||
@@ -18,21 +18,39 @@ 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";
|
||||
import { logger as dbLogger } from "@notesnook/core/dist/logger";
|
||||
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 "@streetwriters/kysely";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import filesystem from "../filesystem";
|
||||
import Storage from "./storage";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
import { getDatabaseKey } from "./encryption";
|
||||
import SettingsService from "../../services/settings";
|
||||
|
||||
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"
|
||||
}
|
||||
);
|
||||
|
||||
export async function setupDatabase(password) {
|
||||
const key = await getDatabaseKey(password);
|
||||
@@ -41,15 +59,6 @@ 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,12 +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 { initialize } from "@notesnook/core";
|
||||
import {
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler
|
||||
} from "@streetwriters/kysely";
|
||||
import { initialize } from "@notesnook/core/dist/logger";
|
||||
import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from "kysely";
|
||||
import { Platform } from "react-native";
|
||||
import { setLogger } from ".";
|
||||
import { RNSqliteDriver } from "./sqlite.kysely";
|
||||
|
||||
@@ -17,12 +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 {
|
||||
DatabaseConnection,
|
||||
Driver,
|
||||
QueryResult
|
||||
} from "@streetwriters/kysely";
|
||||
import { CompiledQuery } from "@streetwriters/kysely";
|
||||
import type { DatabaseConnection, Driver, QueryResult } from "kysely";
|
||||
import { CompiledQuery } from "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
|
||||
removeCryptoKey,
|
||||
decryptMulti,
|
||||
encryptMulti
|
||||
} from "./encryption";
|
||||
import { MMKV } from "./mmkv";
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ 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,20 +18,24 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { getFileNameWithExtension } from "@notesnook/core";
|
||||
import {
|
||||
getFileNameWithExtension,
|
||||
isImage,
|
||||
isDocument
|
||||
} from "@notesnook/core/dist/utils/filename";
|
||||
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 { createCacheDir, exists } from "./io";
|
||||
import { cacheDir, copyFileAsync, releasePermissions } from "./utils";
|
||||
import { createCacheDir, exists } from "./io";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
|
||||
export const FileDownloadStatus = {
|
||||
Success: 1,
|
||||
@@ -54,10 +58,15 @@ export async function downloadAllAttachments() {
|
||||
* @param onProgress
|
||||
* @returns
|
||||
*/
|
||||
export async function downloadAttachments(attachments) {
|
||||
export async function downloadAttachments(
|
||||
attachments,
|
||||
onProgress,
|
||||
canceled,
|
||||
groupId
|
||||
) {
|
||||
await createCacheDir();
|
||||
if (!attachments || !attachments.length) return;
|
||||
const groupId = `download-all-${Date.now()}`;
|
||||
const result = new Map();
|
||||
|
||||
let outputFolder;
|
||||
if (Platform.OS === "android") {
|
||||
@@ -81,117 +90,92 @@ export async function downloadAttachments(attachments) {
|
||||
|
||||
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 {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: groupId,
|
||||
current: i + 1,
|
||||
total: attachments.length,
|
||||
filename: attachment.hash
|
||||
});
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
return;
|
||||
}
|
||||
onProgress?.(
|
||||
i + 1 / attachments.length,
|
||||
`Downloading attachments (${i + 1}/${
|
||||
attachments.length
|
||||
})... Please wait`
|
||||
);
|
||||
// Download to cache
|
||||
let uri = await downloadAttachment(hash, false, {
|
||||
silent: true,
|
||||
cache: true,
|
||||
groupId: groupId
|
||||
});
|
||||
|
||||
if (isCancelled()) return;
|
||||
|
||||
if (canceled.current) {
|
||||
RNFetchBlob.fs.unlink(zipSourceFolder).catch(console.log);
|
||||
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);
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
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`
|
||||
result.set(hash, {
|
||||
filename: attachment.filename,
|
||||
status: FileDownloadStatus.Success,
|
||||
attachment: attachment
|
||||
});
|
||||
});
|
||||
await zip(zipSourceFolder, zipOutputFile);
|
||||
sub?.remove();
|
||||
|
||||
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);
|
||||
} catch (e) {
|
||||
result.set(hash, {
|
||||
filename: attachment.filename,
|
||||
status: FileDownloadStatus.Fail,
|
||||
reason: e
|
||||
});
|
||||
ToastManager.error(e, "Error downloading attachment");
|
||||
}
|
||||
}
|
||||
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.
|
||||
|
||||
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);
|
||||
sub = subscribe(({ progress }) => {
|
||||
onProgress(
|
||||
progress,
|
||||
`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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default async function downloadAttachment(
|
||||
@@ -227,11 +211,7 @@ export default async function downloadAttachment(
|
||||
try {
|
||||
await db
|
||||
.fs()
|
||||
.downloadFile(
|
||||
options.groupId || attachment.hash,
|
||||
attachment.hash,
|
||||
attachment.chunkSize
|
||||
);
|
||||
.downloadFile(options.groupId || attachment.hash, attachment.hash);
|
||||
if (!(await exists(attachment.hash))) {
|
||||
DatabaseLogger.log("Attachment does not exist after download.");
|
||||
return;
|
||||
@@ -279,6 +259,15 @@ 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}`;
|
||||
}
|
||||
@@ -298,6 +287,7 @@ export default async function downloadAttachment(
|
||||
|
||||
return fileUri;
|
||||
} catch (e) {
|
||||
DatabaseLogger.error(e);
|
||||
if (attachment.dateUploaded) {
|
||||
RNFetchBlob.fs
|
||||
.unlink(RNFetchBlob.fs.dirs.CacheDir + `/${attachment.hash}`)
|
||||
@@ -306,7 +296,6 @@ 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,56 +17,32 @@ 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, requestOptions, cancelToken) {
|
||||
if (!requestOptions) {
|
||||
DatabaseLogger.log(
|
||||
`Error downloading file: ${filename}, reason: No requestOptions`
|
||||
);
|
||||
export async function downloadFile(filename, data, cancelToken) {
|
||||
if (!data) {
|
||||
DatabaseLogger.log(`Error downloading file: ${filename}, reason: No data`);
|
||||
return false;
|
||||
}
|
||||
|
||||
DatabaseLogger.log(`Downloading ${filename}`);
|
||||
await createCacheDir();
|
||||
let { url, headers, chunkSize } = requestOptions;
|
||||
let tempFilePath = `${cacheDir}/${filename}_temp`;
|
||||
let originalFilePath = `${cacheDir}/${filename}`;
|
||||
let { url, headers } = data;
|
||||
let path = `${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
|
||||
@@ -87,57 +63,34 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
);
|
||||
throw new Error("Unable to resolve download url");
|
||||
}
|
||||
|
||||
let totalSize = 0;
|
||||
DatabaseLogger.log(`Download starting: ${filename}`);
|
||||
let request = RNFetchBlob.config({
|
||||
path: tempFilePath,
|
||||
IOSBackgroundTask: true,
|
||||
overwrite: true
|
||||
path: path,
|
||||
IOSBackgroundTask: true
|
||||
})
|
||||
.fetch("GET", downloadUrl, null)
|
||||
.progress(async (recieved, total) => {
|
||||
.progress((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;
|
||||
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}`);
|
||||
}
|
||||
|
||||
await fileCheck(response, totalSize);
|
||||
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" && !e.message.includes("NoSuchKey")) {
|
||||
if (e.message !== "canceled") {
|
||||
ToastManager.show({
|
||||
heading: "Error downloading file",
|
||||
message: e.message,
|
||||
@@ -153,8 +106,7 @@ export async function downloadFile(filename, requestOptions, cancelToken) {
|
||||
}
|
||||
|
||||
useAttachmentStore.getState().remove(filename);
|
||||
RNFetchBlob.fs.unlink(tempFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(originalFilePath).catch(console.log);
|
||||
RNFetchBlob.fs.unlink(path).catch(console.log);
|
||||
DatabaseLogger.error(e, {
|
||||
url,
|
||||
headers
|
||||
@@ -163,23 +115,37 @@ export async function downloadFile(filename, requestOptions, 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;
|
||||
if (!isInternetReachable) return { success: true };
|
||||
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. Please upload this file again from the attachment manager. (File hash: ${hash})`
|
||||
};
|
||||
if (size === 0) return { failed: "File length is 0." };
|
||||
} catch (e) {
|
||||
return { failed: e?.message };
|
||||
}
|
||||
|
||||
@@ -17,22 +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 { checkAttachment, downloadFile } from "./download";
|
||||
import { checkAttachment, downloadFile, getUploadedFileSize } from "./download";
|
||||
import {
|
||||
clearFileStorage,
|
||||
deleteFile,
|
||||
exists,
|
||||
readEncrypted,
|
||||
writeEncryptedBase64,
|
||||
hashBase64,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
hashBase64
|
||||
} from "./io";
|
||||
import { uploadFile } from "./upload";
|
||||
import { cancelable, getUploadedFileSize } from "./utils";
|
||||
import { cancelable } from "./utils";
|
||||
|
||||
export default {
|
||||
readEncrypted,
|
||||
@@ -44,10 +39,5 @@ export default {
|
||||
exists,
|
||||
clearFileStorage,
|
||||
getUploadedFileSize,
|
||||
checkAttachment,
|
||||
clearCache,
|
||||
deleteCacheFileByName,
|
||||
deleteCacheFileByPath,
|
||||
bulkExists,
|
||||
getCacheSize
|
||||
checkAttachment
|
||||
};
|
||||
|
||||
@@ -20,10 +20,9 @@ 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 { ABYTES, cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
import { cacheDir, cacheDirOld, getRandomId } from "./utils";
|
||||
|
||||
export async function readEncrypted(filename, key, cipherData) {
|
||||
await migrateFilesFromCache();
|
||||
@@ -159,35 +158,7 @@ export async function migrateFilesFromCache() {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ABYTES = 17;
|
||||
export async function exists(filename) {
|
||||
let path = `${cacheDir}/${filename}`;
|
||||
|
||||
@@ -229,29 +200,3 @@ 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,30 +17,32 @@ 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 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 { cacheDir } from "./utils";
|
||||
import { isImage, isDocument } from "@notesnook/core/dist/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import { IOS_APPGROUPID } from "../../utils/constants";
|
||||
import { createCacheDir } from "./io";
|
||||
import { cacheDir, getUploadedFileSize } from "./utils";
|
||||
import { getUploadedFileSize } from "./download";
|
||||
import { ToastManager } from "../../services/event-manager";
|
||||
|
||||
export async function uploadFile(filename, requestOptions, cancelToken) {
|
||||
if (!requestOptions) return false;
|
||||
let { url, headers } = requestOptions;
|
||||
export async function uploadFile(filename, data, cancelToken) {
|
||||
if (!data) return false;
|
||||
let { url, headers } = data;
|
||||
await createCacheDir();
|
||||
DatabaseLogger.info(`Preparing to upload file: ${filename}`);
|
||||
|
||||
try {
|
||||
const uploadedFileSize = await getUploadedFileSize(filename);
|
||||
|
||||
if (uploadedFileSize === -1) {
|
||||
const error = `Uploaded file verification failed. (File hash: ${filename})`;
|
||||
throw new Error(error);
|
||||
DatabaseLogger.log("Upload verification failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (uploadedFileSize !== 0) {
|
||||
const isUploaded = uploadedFileSize !== 0;
|
||||
if (isUploaded) {
|
||||
DatabaseLogger.log(`File ${filename} is already uploaded.`);
|
||||
return true;
|
||||
}
|
||||
@@ -115,6 +117,9 @@ export async function uploadFile(filename, requestOptions, 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(
|
||||
|
||||
@@ -17,13 +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 * 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 =
|
||||
@@ -37,36 +34,24 @@ export function getRandomId(prefix) {
|
||||
.replace("0.", prefix || "");
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string | undefined} data
|
||||
* @returns
|
||||
*/
|
||||
export function parseS3Error(data) {
|
||||
const xml = typeof data === "string" ? data : null;
|
||||
export function extractValueFromXmlTag(code, xml) {
|
||||
if (!xml.includes(code)) return `Unknown ${code}`;
|
||||
return xml.slice(
|
||||
xml.indexOf(`<${code}>`) + code.length + 2,
|
||||
xml.indexOf(`</${code}>`)
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
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}`);
|
||||
}
|
||||
return error;
|
||||
} catch (e) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,11 +59,12 @@ export function cancelable(operation) {
|
||||
const cancelToken = {
|
||||
cancel: () => {}
|
||||
};
|
||||
return (filename, requestOptions) => {
|
||||
return (filename, { url, headers }) => {
|
||||
return {
|
||||
execute: () => operation(filename, requestOptions, cancelToken),
|
||||
execute: () => operation(filename, { url, headers }, cancelToken),
|
||||
cancel: async () => {
|
||||
await cancelToken.cancel();
|
||||
RNFetchBlob.fs.unlink(`${cacheDir}/${filename}`).catch(console.log);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -105,21 +91,3 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,6 @@ 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,
|
||||
@@ -51,12 +49,14 @@ 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);
|
||||
fwdRef.current?.hide();
|
||||
eSendEvent(eCloseSheet, contextId);
|
||||
},
|
||||
icon: "download"
|
||||
},
|
||||
@@ -118,14 +118,12 @@ const Actions = ({
|
||||
setLoading({
|
||||
name: "Run file check"
|
||||
});
|
||||
const result = await filesystem.checkAttachment(attachment.hash);
|
||||
if (!result) return;
|
||||
|
||||
if (result.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, result.failed);
|
||||
setFailed(result.failed);
|
||||
const res = await filesystem.checkAttachment(attachment.hash);
|
||||
if (res.failed) {
|
||||
db.attachments.markAsFailed(attachment.id, res.failed);
|
||||
setFailed(res.failed);
|
||||
ToastManager.show({
|
||||
heading: "File check failed with error: " + result.failed,
|
||||
heading: "File check failed with error: " + res.failed,
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
@@ -149,8 +147,9 @@ const Actions = ({
|
||||
},
|
||||
{
|
||||
name: "Rename",
|
||||
onPress: async () => {
|
||||
onPress: () => {
|
||||
presentDialog({
|
||||
context: contextId as any,
|
||||
input: true,
|
||||
title: "Rename file",
|
||||
paragraph: "Enter a new name for the file",
|
||||
@@ -218,7 +217,6 @@ const Actions = ({
|
||||
style={{
|
||||
maxHeight: "100%"
|
||||
}}
|
||||
keyboardShouldPersistTaps="never"
|
||||
>
|
||||
<Dialog context={contextId} />
|
||||
<SheetProvider context={contextId} />
|
||||
|
||||
@@ -22,6 +22,7 @@ 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";
|
||||
@@ -43,8 +44,7 @@ export const AttachmentItem = ({
|
||||
setAttachments,
|
||||
pressable = true,
|
||||
hideWhenNotDownloading,
|
||||
context,
|
||||
errorOnly
|
||||
context
|
||||
}: {
|
||||
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,7 +67,8 @@ export const AttachmentItem = ({
|
||||
Actions.present(attachment, setAttachments, context);
|
||||
};
|
||||
|
||||
return errorOnly && attachment && !attachment?.failed ? null : (
|
||||
return hideWhenNotDownloading &&
|
||||
(!currentProgress || !currentProgress.value) ? null : (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={onPress}
|
||||
@@ -77,6 +78,8 @@ export const AttachmentItem = ({
|
||||
justifyContent: "space-between",
|
||||
padding: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
minHeight: 45
|
||||
}}
|
||||
>
|
||||
@@ -86,46 +89,41 @@ export const AttachmentItem = ({
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginLeft: -5,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.secondary.border,
|
||||
paddingHorizontal: 2,
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 5
|
||||
marginLeft: -5
|
||||
}}
|
||||
>
|
||||
<Icon name="file" size={SIZE.xxxl} color={colors.primary.icon} />
|
||||
|
||||
<Paragraph
|
||||
adjustsFontSizeToFit
|
||||
size={8}
|
||||
color={colors.secondary.paragraph}
|
||||
size={6}
|
||||
color={colors.static.white}
|
||||
style={{
|
||||
maxWidth: 30
|
||||
position: "absolute"
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{getFileExtension(attachment.filename).toUpperCase() ||
|
||||
attachment.mimeType.split("/")?.[1]?.toUpperCase()}
|
||||
{getFileExtension(attachment.filename).toUpperCase()}
|
||||
</Paragraph>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexShrink: 1
|
||||
flexShrink: 1,
|
||||
marginLeft: 10
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.sm}
|
||||
size={SIZE.sm - 1}
|
||||
style={{
|
||||
flexWrap: "wrap"
|
||||
flexWrap: "wrap",
|
||||
marginBottom: 2.5
|
||||
}}
|
||||
numberOfLines={1}
|
||||
lineBreakMode="middle"
|
||||
@@ -135,8 +133,11 @@ export const AttachmentItem = ({
|
||||
</Paragraph>
|
||||
|
||||
{!hideWhenNotDownloading ? (
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xxs}>
|
||||
File size: {formatBytes(attachment.size)}
|
||||
<Paragraph color={colors.secondary.paragraph} size={SIZE.xs}>
|
||||
{formatBytes(attachment.size)}{" "}
|
||||
{currentProgress?.type
|
||||
? "(" + currentProgress.type + "ing - tap to cancel)"
|
||||
: ""}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
297
apps/mobile/app/components/attachments/download-attachments.tsx
Normal file
297
apps/mobile/app/components/attachments/download-attachments.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
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,28 +19,21 @@ 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, View } from "react-native";
|
||||
import { ActivityIndicator, ScrollView, 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 { 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 { presentSheet } from "../../services/event-manager";
|
||||
import { useSettingStore } from "../../stores/use-setting-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";
|
||||
@@ -49,105 +42,22 @@ 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"
|
||||
};
|
||||
|
||||
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;
|
||||
}) => {
|
||||
export const AttachmentDialog = ({ note }: { note?: Note }) => {
|
||||
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) {
|
||||
@@ -158,7 +68,6 @@ export const AttachmentDialog = ({
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => {
|
||||
setLoading(false);
|
||||
setAttachments(attachments);
|
||||
});
|
||||
} else {
|
||||
@@ -167,10 +76,7 @@ export const AttachmentDialog = ({
|
||||
...DEFAULT_SORTING,
|
||||
sortBy: "dateModified"
|
||||
})
|
||||
.then((attachments) => {
|
||||
setAttachments(attachments);
|
||||
setLoading(false);
|
||||
});
|
||||
.then((attachments) => setAttachments(attachments));
|
||||
}
|
||||
}, [note]);
|
||||
|
||||
@@ -202,71 +108,53 @@ export const AttachmentDialog = ({
|
||||
setAttachments={async () => {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
}}
|
||||
errorOnly={currentFilter === "errors"}
|
||||
attachments={attachments}
|
||||
id={index}
|
||||
context="global"
|
||||
context="attachments-list"
|
||||
/>
|
||||
);
|
||||
|
||||
const onCheck = async () => {
|
||||
const getState = () =>
|
||||
useRechecker.getState().progress[note?.id || "all"] || {};
|
||||
const setState = (state: Partial<RecheckerProgress>) =>
|
||||
useRechecker.getState().setProgress(note?.id || "all", {
|
||||
...getState(),
|
||||
...state
|
||||
});
|
||||
if (!attachments) return;
|
||||
setLoading(true);
|
||||
|
||||
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;
|
||||
}
|
||||
for (let i = 0; i < attachments.placeholders.length; i++) {
|
||||
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 {
|
||||
setState({
|
||||
passed: getState().passed + 1
|
||||
});
|
||||
await db.attachments.markAsFailed(attachment.id);
|
||||
await db.attachments.markAsFailed(attachment.id, undefined);
|
||||
}
|
||||
}
|
||||
if (filter === useRechecker.getState().currentFilter) {
|
||||
setAttachments(await filterAttachments(currentFilter));
|
||||
}
|
||||
|
||||
setState({
|
||||
isWorking: false
|
||||
});
|
||||
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"
|
||||
}
|
||||
];
|
||||
|
||||
const filterAttachments = async (type: string) => {
|
||||
let items: FilteredSelector<Attachment> = db.attachments.all;
|
||||
|
||||
@@ -295,14 +183,6 @@ export const AttachmentDialog = ({
|
||||
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({
|
||||
@@ -312,59 +192,39 @@ export const AttachmentDialog = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{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={{
|
||||
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>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 12
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<Heading>Attachments</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40,
|
||||
marginRight: 10
|
||||
}}
|
||||
size={SIZE.lg}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
name="check-all"
|
||||
style={{
|
||||
@@ -376,243 +236,126 @@ export const AttachmentDialog = ({
|
||||
size={SIZE.lg}
|
||||
onPress={onCheck}
|
||||
/>
|
||||
)}
|
||||
|
||||
<IconButton
|
||||
name="download"
|
||||
style={{
|
||||
height: 40,
|
||||
width: 40
|
||||
}}
|
||||
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}
|
||||
/>
|
||||
</View>
|
||||
<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>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 12,
|
||||
height: "100%"
|
||||
<Seperator />
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
}}
|
||||
>
|
||||
<Seperator />
|
||||
<Input
|
||||
placeholder="Filter attachments by filename, type or hash"
|
||||
onChangeText={onChangeText}
|
||||
onSubmit={() => {
|
||||
onChangeText(attachmentSearchValue.current as string);
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
|
||||
{rechecker.shown ? (
|
||||
<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}
|
||||
style={{
|
||||
borderRadius: 0,
|
||||
borderBottomWidth: 1,
|
||||
flexGrow: 1,
|
||||
borderBottomColor:
|
||||
currentFilter !== item.filterBy
|
||||
? "transparent"
|
||||
: colors.primary.accent
|
||||
}}
|
||||
onPress={async () => {
|
||||
setCurrentFilter(item.filterBy);
|
||||
setAttachments(await filterAttachments(item.filterBy));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
<FlashList
|
||||
keyboardDismissMode="none"
|
||||
keyboardShouldPersistTaps="always"
|
||||
ListEmptyComponent={
|
||||
<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={{
|
||||
height: 150,
|
||||
justifyContent: "center",
|
||||
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]
|
||||
}
|
||||
<Icon name="attachment" size={60} color={colors.secondary.icon} />
|
||||
<Paragraph>
|
||||
{note ? "No attachments on this note" : "No attachments"}
|
||||
</Paragraph>
|
||||
</View>
|
||||
}
|
||||
: state
|
||||
);
|
||||
ListFooterComponent={
|
||||
<View
|
||||
style={{
|
||||
height: 350
|
||||
}}
|
||||
/>
|
||||
}
|
||||
estimatedItemSize={50}
|
||||
data={attachments?.placeholders}
|
||||
renderItem={renderItem}
|
||||
/>
|
||||
|
||||
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} />
|
||||
))}
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
style={{
|
||||
textAlign: "center",
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
name="shield-key-outline"
|
||||
size={SIZE.xs}
|
||||
color={colors.primary.icon}
|
||||
/>
|
||||
{" "}All attachments are end-to-end encrypted.
|
||||
</Paragraph>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
AttachmentDialog.present = (note?: Note) => {
|
||||
presentSheet({
|
||||
component: () => <AttachmentDialog note={note} isSheet={true} />,
|
||||
component: () => <AttachmentDialog note={note} />,
|
||||
keyboardHandlerDisabled: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -19,6 +19,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
@@ -28,7 +31,9 @@ import { eCloseLoginDialog, eOpenLoginDialog } from "../../utils/events";
|
||||
import { sleep } from "../../utils/time";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { Toast } from "../toast";
|
||||
import { initialAuthMode } from "./common";
|
||||
import { Button } from "../ui/button";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { hideAuth, initialAuthMode } from "./common";
|
||||
import { Login } from "./login";
|
||||
import { Signup } from "./signup";
|
||||
|
||||
@@ -44,6 +49,7 @@ const AuthModal = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [currentAuthMode, setCurrentAuthMode] = useState(AuthMode.login);
|
||||
const actionSheetRef = useRef();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
|
||||
useEffect(() => {
|
||||
eSubscribeEvent(eOpenLoginDialog, open);
|
||||
@@ -91,18 +97,81 @@ const AuthModal = () => {
|
||||
centered={false}
|
||||
enableSheetKeyboardHandler
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
<KeyboardAwareScrollView
|
||||
style={{
|
||||
width: "100%"
|
||||
}}
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{currentAuthMode !== AuthMode.login ? (
|
||||
<Signup
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
trial={AuthMode.trialSignup === currentAuthMode}
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
/>
|
||||
) : (
|
||||
<Login
|
||||
welcome={initialAuthMode.current === AuthMode.welcomeSignup}
|
||||
changeMode={(mode) => setCurrentAuthMode(mode)}
|
||||
/>
|
||||
)}
|
||||
</KeyboardAwareScrollView>
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
paddingTop: Platform.OS === "android" ? 0 : insets.top,
|
||||
top: 0,
|
||||
zIndex: 999,
|
||||
backgroundColor: colors.secondary.background,
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 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>
|
||||
|
||||
<Toast context="local" />
|
||||
</BaseDialog>
|
||||
|
||||
@@ -68,8 +68,7 @@ export const ChangePassword = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await BackupService.run(false, "change-password-dialog");
|
||||
if (result.error)
|
||||
throw new Error(`Failed to create backup: ${result.error}`);
|
||||
if (!result) throw new Error("Failed to create backup");
|
||||
|
||||
await db.user.clearSessions();
|
||||
await db.user.changePassword(oldPassword.current, password.current);
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { 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>
|
||||
);
|
||||
};
|
||||
@@ -21,7 +21,6 @@ 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 Sync from "../../services/sync";
|
||||
@@ -90,233 +89,220 @@ export const Login = ({ changeMode }) => {
|
||||
<>
|
||||
<ForgotPassword />
|
||||
<SheetProvider context="two_factor_verify" />
|
||||
<KeyboardAwareScrollView
|
||||
<View
|
||||
style={{
|
||||
width: "100%"
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center"
|
||||
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={{
|
||||
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
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: 20,
|
||||
height: 5,
|
||||
backgroundColor: colors.secondary.background,
|
||||
borderRadius: 2
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<Heading
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
marginTop: 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%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20
|
||||
}}
|
||||
>
|
||||
<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="Enter your email"
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel="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"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25
|
||||
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%",
|
||||
backgroundColor: colors.primary.background,
|
||||
alignSelf: "center",
|
||||
paddingHorizontal: 20
|
||||
}}
|
||||
>
|
||||
<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="Enter your email"
|
||||
defaultValue={email.current}
|
||||
editable={step === LoginSteps.emailAuth && !loading}
|
||||
onSubmit={() => {
|
||||
if (step === LoginSteps.emailAuth) {
|
||||
login();
|
||||
} else {
|
||||
passwordInputRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{step === LoginSteps.passwordAuth && (
|
||||
<>
|
||||
<Input
|
||||
fwdRef={passwordInputRef}
|
||||
onChangeText={(value) => {
|
||||
password.current = value;
|
||||
}}
|
||||
testID="input.password"
|
||||
returnKeyLabel="Done"
|
||||
returnKeyType="done"
|
||||
secureTextEntry
|
||||
autoComplete="password"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
placeholder="Password"
|
||||
marginBottom={0}
|
||||
editable={!loading}
|
||||
defaultValue={password.current}
|
||||
onSubmit={() => login()}
|
||||
/>
|
||||
<Button
|
||||
loading={loading}
|
||||
title="Forgot your password?"
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
height: 30,
|
||||
paddingHorizontal: 0
|
||||
}}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
SheetManager.show("forgotpassword_sheet", email.current);
|
||||
}}
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
textStyle={{
|
||||
textDecorationLine: "underline"
|
||||
}}
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
fontSize={SIZE.xs}
|
||||
type="plain"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{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"
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
marginTop: 25
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
loading={loading}
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
login();
|
||||
}}
|
||||
style={{
|
||||
width: 250,
|
||||
borderRadius: 100
|
||||
}}
|
||||
height={50}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title={!loading ? "Continue" : null}
|
||||
/>
|
||||
|
||||
{!loading ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (loading) return;
|
||||
changeMode(1);
|
||||
}}
|
||||
activeOpacity={0.8}
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: 12,
|
||||
paddingVertical: 12
|
||||
}}
|
||||
{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
|
||||
}}
|
||||
>
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
>
|
||||
Don't have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
color={colors.secondary.paragraph}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
Don't have an account?{" "}
|
||||
<Paragraph
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
Sign up
|
||||
</Paragraph>
|
||||
Sign up
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,38 +17,26 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { 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 { useSettingStore } from "../../stores/use-setting-store";
|
||||
import PremiumService from "../../services/premium";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { useUserStore } from "../../stores/use-user-store";
|
||||
import { openLinkInBrowser } from "../../utils/functions";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { Loading } from "../loading";
|
||||
import { PaywallComponent } from "../premium/component";
|
||||
import { sleep } from "../../utils/time";
|
||||
import { Button } from "../ui/button";
|
||||
import Input from "../ui/input";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
import { hideAuth } from "./common";
|
||||
import { useSettingStore } from "../../stores/use-setting-store";
|
||||
import SettingsService from "../../services/settings";
|
||||
import { AuthHeader } from "./header";
|
||||
import { SignupContext } from "./signup-context";
|
||||
|
||||
const SignupSteps = {
|
||||
signup: 0,
|
||||
selectPlan: 1,
|
||||
createAccount: 2
|
||||
};
|
||||
|
||||
export const Signup = ({ changeMode, welcome }) => {
|
||||
const [currentStep, setCurrentStep] = useState(SignupSteps.signup);
|
||||
export const Signup = ({ changeMode, trial }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const email = useRef();
|
||||
const emailInputRef = useRef();
|
||||
@@ -61,7 +49,7 @@ export const Signup = ({ changeMode, welcome }) => {
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const setLastSynced = useUserStore((state) => state.setLastSynced);
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const { height } = useWindowDimensions();
|
||||
const { width, height } = useWindowDimensions();
|
||||
const validateInfo = () => {
|
||||
if (!password.current || !email.current || !confirmPassword.current) {
|
||||
ToastManager.show({
|
||||
@@ -80,20 +68,22 @@ export const Signup = ({ changeMode, welcome }) => {
|
||||
const signup = async () => {
|
||||
if (!validateInfo() || error) return;
|
||||
if (loading) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
setCurrentStep(SignupSteps.createAccount);
|
||||
await db.user.signup(email.current.toLowerCase(), password.current);
|
||||
let user = await db.user.getUser();
|
||||
setUser(user);
|
||||
setLastSynced(await db.lastSynced());
|
||||
clearMessage();
|
||||
setEmailVerifyMessage();
|
||||
setCurrentStep(SignupSteps.selectPlan);
|
||||
return true;
|
||||
hideAuth();
|
||||
await sleep(300);
|
||||
if (trial) {
|
||||
PremiumService.sheet(null, null, true);
|
||||
} else {
|
||||
PremiumService.showVerifyEmailDialog();
|
||||
}
|
||||
} catch (e) {
|
||||
setCurrentStep(SignupSteps.signup);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
heading: "Signup failed",
|
||||
@@ -101,266 +91,215 @@ export const Signup = ({ changeMode, welcome }) => {
|
||||
type: "error",
|
||||
context: "local"
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SignupContext.Provider
|
||||
value={{
|
||||
signup: signup
|
||||
}}
|
||||
>
|
||||
{currentStep === SignupSteps.signup ? (
|
||||
<>
|
||||
<AuthHeader welcome={welcome} />
|
||||
<KeyboardAwareScrollView
|
||||
<>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
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
|
||||
style={{
|
||||
width: "100%"
|
||||
flexDirection: "row"
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
minHeight: "90%"
|
||||
}}
|
||||
nestedScrollEnabled
|
||||
enableAutomaticScroll={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
borderRadius: DDS.isTab ? 5 : 0,
|
||||
backgroundColor: colors.primary.background,
|
||||
zIndex: 10,
|
||||
width: "100%",
|
||||
alignSelf: "center",
|
||||
height: "100%"
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
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}
|
||||
>
|
||||
<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
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 5,
|
||||
backgroundColor: colors.primary.accent,
|
||||
borderRadius: 2,
|
||||
marginRight: 7
|
||||
}}
|
||||
/>
|
||||
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>
|
||||
|
||||
<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>
|
||||
<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
|
||||
style={{
|
||||
marginBottom: 25,
|
||||
paddingHorizontal: 16,
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.xs}
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs + 1}
|
||||
style={{ color: colors.primary.accent }}
|
||||
>
|
||||
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.
|
||||
Login
|
||||
</Paragraph>
|
||||
</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>
|
||||
</Paragraph>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,9 +18,8 @@ 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 { ToastManager, eSendEvent } from "../../services/event-manager";
|
||||
import { eSendEvent, ToastManager } from "../../services/event-manager";
|
||||
import { clearMessage } from "../../services/message";
|
||||
import PremiumService from "../../services/premium";
|
||||
import SettingsService from "../../services/settings";
|
||||
@@ -34,18 +33,15 @@ export const LoginSteps = {
|
||||
passwordAuth: 3
|
||||
};
|
||||
|
||||
export const useLogin = (
|
||||
onFinishLogin?: () => void,
|
||||
sessionExpired = false
|
||||
) => {
|
||||
export const useLogin = (onFinishLogin, sessionExpired = false) => {
|
||||
const [error, setError] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
const [step, setStep] = useState(LoginSteps.emailAuth);
|
||||
const email = useRef<string>();
|
||||
const password = useRef<string>();
|
||||
const emailInputRef = useRef<TextInput>(null);
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const email = useRef();
|
||||
const password = useRef();
|
||||
const emailInputRef = useRef();
|
||||
const passwordInputRef = useRef();
|
||||
|
||||
const validateInfo = () => {
|
||||
if (
|
||||
@@ -72,57 +68,44 @@ export const useLogin = (
|
||||
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: { code: string; method: string },
|
||||
callback: (value: boolean) => void
|
||||
) => {
|
||||
try {
|
||||
const success = await db.user.authenticateMultiFactorCode(
|
||||
mfa.code,
|
||||
mfa.method
|
||||
);
|
||||
TwoFactorVerification.present(async (mfa, callback) => {
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (success) {
|
||||
setStep(LoginSteps.passwordAuth);
|
||||
setLoading(false);
|
||||
setTimeout(() => {
|
||||
passwordInputRef.current?.focus();
|
||||
}, 1);
|
||||
callback && callback(true);
|
||||
}
|
||||
},
|
||||
mfaInfo
|
||||
);
|
||||
callback && callback(false);
|
||||
} catch (e) {
|
||||
callback && callback(false);
|
||||
if (e.message === "invalid_grant") {
|
||||
eSendEvent(eCloseSheet, "two_factor_verify");
|
||||
setLoading(false);
|
||||
setStep(LoginSteps.emailAuth);
|
||||
}
|
||||
}
|
||||
}, 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,
|
||||
undefined,
|
||||
null,
|
||||
sessionExpired
|
||||
);
|
||||
finishLogin();
|
||||
@@ -131,11 +114,11 @@ export const useLogin = (
|
||||
}
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
finishWithError(e as Error);
|
||||
finishWithError(e);
|
||||
}
|
||||
};
|
||||
|
||||
const finishWithError = async (e: Error) => {
|
||||
const finishWithError = async (e) => {
|
||||
if (e.message === "invalid_grant") setStep(LoginSteps.emailAuth);
|
||||
setLoading(false);
|
||||
ToastManager.show({
|
||||
@@ -23,7 +23,6 @@ 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";
|
||||
@@ -36,7 +35,8 @@ import { Expiring } from "../premium/expiring";
|
||||
import SheetProvider from "../sheet-provider";
|
||||
import RateAppSheet from "../sheets/rate-app";
|
||||
import RecoveryKeySheet from "../sheets/recovery-key";
|
||||
import Progress from "../dialogs/progress";
|
||||
import RestoreDataSheet from "../sheets/restore-data";
|
||||
import { AppLockPassword } from "../dialogs/applock-password";
|
||||
|
||||
const DialogProvider = () => {
|
||||
const { colors } = useThemeColors();
|
||||
@@ -52,6 +52,7 @@ const DialogProvider = () => {
|
||||
<RecoveryKeySheet colors={colors} />
|
||||
<SheetProvider />
|
||||
<SheetProvider context="sync_progress" />
|
||||
<RestoreDataSheet />
|
||||
<ResultDialog />
|
||||
<VaultDialog colors={colors} />
|
||||
<RateAppSheet />
|
||||
@@ -61,7 +62,6 @@ const DialogProvider = () => {
|
||||
<SessionExpired />
|
||||
<PDFPreview />
|
||||
<JumpToSectionDialog />
|
||||
<Progress />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -32,29 +32,21 @@ const DialogContainer = ({
|
||||
}: ViewProps & {
|
||||
width?: any;
|
||||
height?: any;
|
||||
noBorder?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...restProps}
|
||||
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)
|
||||
}
|
||||
]}
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ type DialogInfo = {
|
||||
paragraph?: string;
|
||||
positiveText?: string;
|
||||
negativeText?: string;
|
||||
positivePress?: (...args: any[]) => void;
|
||||
positivePress?: (value: any) => void;
|
||||
onClose?: () => void;
|
||||
positiveType?:
|
||||
| "transparent"
|
||||
@@ -45,15 +45,6 @@ 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,7 +37,6 @@ 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();
|
||||
@@ -63,8 +62,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
disableBackdropClosing: false,
|
||||
check: {
|
||||
info: "Check",
|
||||
type: "transparent",
|
||||
defaultValue: false
|
||||
type: "transparent"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,7 +86,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
if (!data.context) data.context = "global";
|
||||
if (data.context !== context) return;
|
||||
setDialogInfo(data);
|
||||
setChecked(data.check?.defaultValue);
|
||||
setChecked(false);
|
||||
values.current.inputValue = data.defaultValue;
|
||||
setVisible(true);
|
||||
},
|
||||
@@ -182,19 +180,6 @@ 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
|
||||
@@ -213,7 +198,7 @@ export const Dialog = ({ context = "global" }) => {
|
||||
iconSize={20}
|
||||
width="100%"
|
||||
title={dialogInfo.check.info}
|
||||
type={checked ? dialogInfo.check.type || "plain" : "plain"}
|
||||
type={checked ? dialogInfo.check.type || "selected" : "plain"}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -44,7 +44,6 @@ 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;
|
||||
@@ -125,7 +124,6 @@ const PDFPreview = () => {
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
deleteCacheFileByPath(pdfSource);
|
||||
setPDFSource(null);
|
||||
setVisible(false);
|
||||
setPassword("");
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { 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);
|
||||
}
|
||||
@@ -67,6 +67,8 @@ export const Header = ({
|
||||
hasSearch?: boolean;
|
||||
onSearch?: () => void;
|
||||
}) => {
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { colors } = useThemeColors();
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const [borderHidden, setBorderHidden] = useState(true);
|
||||
|
||||
@@ -96,8 +96,7 @@ export const RightMenus = ({
|
||||
animationDuration={200}
|
||||
style={{
|
||||
borderRadius: 5,
|
||||
backgroundColor: contextMenuColors.primary.background,
|
||||
marginTop: -40
|
||||
backgroundColor: contextMenuColors.primary.background
|
||||
}}
|
||||
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";
|
||||
import type { ImageAttributes } from "@notesnook/editor";
|
||||
import dataurl from "@notesnook/core/dist/utils/dataurl";
|
||||
import type { ImageAttributes } from "@notesnook/editor/dist/extensions/image/index";
|
||||
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,51 +19,38 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { Linking, ScrollView, useWindowDimensions, View } from "react-native";
|
||||
import {
|
||||
Linking,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View
|
||||
} from "react-native";
|
||||
import { SwiperFlatList } from "react-native-swiper-flatlist";
|
||||
import { eSendEvent } from "../../services/event-manager";
|
||||
import Navigation from "../../services/navigation";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { getElevationStyle } from "../../utils/elevation";
|
||||
import useGlobalSafeAreaInsets from "../../hooks/use-global-safe-area-insets";
|
||||
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";
|
||||
|
||||
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 Intro = ({ navigation }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const { width } = useWindowDimensions();
|
||||
const isTelemetryEnabled = useSettingStore(
|
||||
(state) => state.settings.telemetry
|
||||
);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const insets = useGlobalSafeAreaInsets();
|
||||
const renderItem = React.useCallback(
|
||||
({ item }: { item: IntroItem }) => (
|
||||
({ item }) => (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
@@ -118,18 +105,17 @@ const Intro = () => {
|
||||
|
||||
{item.body ? <Paragraph size={SIZE.sm}>{item.body}</Paragraph> : null}
|
||||
|
||||
{item.testimonial ? (
|
||||
{item.tesimonial ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
fontStyle: "italic",
|
||||
fontSize: SIZE.lg
|
||||
}}
|
||||
onPress={() => {
|
||||
if (!item.link) return;
|
||||
Linking.openURL(item.link);
|
||||
}}
|
||||
>
|
||||
{item.testimonial} — {item.user}
|
||||
{item.tesimonial} — {item.user}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
</View>
|
||||
@@ -144,23 +130,22 @@ const Intro = () => {
|
||||
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 : undefined,
|
||||
borderColor:
|
||||
deviceMode !== "mobile" ? colors.primary.border : undefined,
|
||||
borderRadius: deviceMode !== "mobile" ? 20 : undefined,
|
||||
marginTop: deviceMode !== "mobile" ? 50 : undefined,
|
||||
flexGrow: 2
|
||||
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
|
||||
}}
|
||||
>
|
||||
<SwiperFlatList
|
||||
@@ -170,7 +155,26 @@ const Intro = () => {
|
||||
index={0}
|
||||
useReactNativeGestureHandler={true}
|
||||
showPagination
|
||||
data={introItems}
|
||||
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"
|
||||
}
|
||||
]}
|
||||
paginationActiveColor={colors.primary.accent}
|
||||
paginationStyleItem={{
|
||||
width: 10,
|
||||
@@ -186,14 +190,13 @@ const Intro = () => {
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
paddingHorizontal: 16,
|
||||
gap: 12,
|
||||
paddingVertical: 16
|
||||
justifyContent: "center",
|
||||
minHeight: height * 0.3
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
width="100%"
|
||||
onPress={() => {
|
||||
width={250}
|
||||
onPress={async () => {
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.welcomeSignup);
|
||||
setTimeout(() => {
|
||||
SettingsService.set({
|
||||
@@ -207,29 +210,51 @@ const Intro = () => {
|
||||
style={{
|
||||
paddingHorizontal: 24,
|
||||
alignSelf: "center",
|
||||
...getElevationStyle(5)
|
||||
...getElevationStyle(5),
|
||||
borderRadius: 100
|
||||
}}
|
||||
fontSize={SIZE.md}
|
||||
type="accent"
|
||||
title="Get started"
|
||||
/>
|
||||
|
||||
<Button
|
||||
width="100%"
|
||||
title="I already have an account"
|
||||
type="secondary"
|
||||
onPress={() => {
|
||||
eSendEvent(eOpenLoginDialog, AuthMode.login);
|
||||
setTimeout(() => {
|
||||
SettingsService.set({
|
||||
introCompleted: true
|
||||
});
|
||||
Navigation.replace("Notes", {
|
||||
canGoBack: false
|
||||
});
|
||||
}, 1000);
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
width: "90%",
|
||||
marginBottom: 12,
|
||||
paddingHorizontal: 12,
|
||||
justifyContent: "center",
|
||||
padding: 12,
|
||||
maxWidth: 500
|
||||
}}
|
||||
/>
|
||||
onPress={() => {
|
||||
SettingsService.set({ telemetry: !isTelemetryEnabled });
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
125
apps/mobile/app/components/intro/welcome.js
Normal file
125
apps/mobile/app/components/intro/welcome.js
Normal file
File diff suppressed because one or more lines are too long
@@ -36,7 +36,7 @@ import {
|
||||
TrashItem,
|
||||
VirtualizedGrouping
|
||||
} from "@notesnook/core";
|
||||
import { getSortValue } from "@notesnook/core";
|
||||
import { getSortValue } from "@notesnook/core/dist/utils/grouping";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { ProgressBarComponent } from "../ui/svg/lazy";
|
||||
import Heading from "../ui/typography/heading";
|
||||
import Paragraph from "../ui/typography/paragraph";
|
||||
export const Loading = (props: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 16
|
||||
}}
|
||||
>
|
||||
{props.icon ? (
|
||||
<Icon name={props.icon} size={80} color={colors.primary.accent} />
|
||||
) : null}
|
||||
|
||||
{props.title ? (
|
||||
<Heading
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</Heading>
|
||||
) : null}
|
||||
|
||||
{props.description ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
>
|
||||
{props.description}
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
marginTop: 15
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
292
apps/mobile/app/components/premium/component.js
Normal file
292
apps/mobile/app/components/premium/component.js
Normal file
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
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>
|
||||
);
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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,8 +24,7 @@ import {
|
||||
} from "../../services/event-manager";
|
||||
import { eClosePremiumDialog, eOpenPremiumDialog } from "../../utils/events";
|
||||
import BaseDialog from "../dialog/base-dialog";
|
||||
import { PaywallComponent } from "./component";
|
||||
import { IconButton } from "../ui/icon-button";
|
||||
import { Component } from "./component";
|
||||
|
||||
class PremiumDialog extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -75,7 +74,7 @@ class PremiumDialog extends React.Component {
|
||||
background={this.props.colors.primary.background}
|
||||
onRequestClose={this.onClose}
|
||||
>
|
||||
<PaywallComponent
|
||||
<Component
|
||||
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,7 +48,6 @@ 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";
|
||||
@@ -93,21 +92,173 @@ export const PricingPlans = ({
|
||||
compact?: boolean;
|
||||
}) => {
|
||||
const { colors } = useThemeColors();
|
||||
const {
|
||||
buySubscription,
|
||||
buying,
|
||||
getStandardPrice,
|
||||
product,
|
||||
setBuying,
|
||||
loading,
|
||||
user,
|
||||
setProduct,
|
||||
monthlyPlan,
|
||||
yearlyPlan,
|
||||
getPromo
|
||||
} = usePricingPlans({
|
||||
promoOffer: promo
|
||||
});
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return loading ? (
|
||||
<View
|
||||
@@ -127,215 +278,354 @@ export const PricingPlans = ({
|
||||
}}
|
||||
>
|
||||
{buying ? (
|
||||
<BaseDialog visible statusBarTranslucent centered>
|
||||
<BaseDialog statusBarTranslucent centered>
|
||||
<ActivityIndicator size={50} color="white" />
|
||||
</BaseDialog>
|
||||
) : null}
|
||||
|
||||
{!user && !product ? (
|
||||
{!upgrade ? (
|
||||
<>
|
||||
{heading || (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<>
|
||||
{monthlyPlan && (monthlyPlan?.info?.discount || 0) > 0 ? (
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
marginTop: marginTop || 20,
|
||||
marginBottom: 20
|
||||
<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
|
||||
style={{
|
||||
textAlign: "center"
|
||||
}}
|
||||
color={colors.primary.accent}
|
||||
>
|
||||
Get {monthlyPlan?.info?.discount}% off in{" "}
|
||||
{monthlyPlan?.info?.country}
|
||||
</Heading>
|
||||
</View>
|
||||
strikethrough={true}
|
||||
/>
|
||||
) : (
|
||||
<PricingItem
|
||||
onPress={() => {
|
||||
if (!monthlyPlan?.product) return;
|
||||
buySubscription(monthlyPlan?.product);
|
||||
}}
|
||||
product={{
|
||||
type: "yearly",
|
||||
data: yearlyPlan?.product,
|
||||
info: "Pay once a year, cancel anytime."
|
||||
}}
|
||||
strikethrough={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Heading
|
||||
style={{
|
||||
paddingTop: 15,
|
||||
fontSize: SIZE.lg
|
||||
}}
|
||||
>
|
||||
Special offer for you
|
||||
</Heading>
|
||||
|
||||
<View
|
||||
style={{
|
||||
paddingVertical: 20,
|
||||
paddingBottom: 10
|
||||
}}
|
||||
>
|
||||
<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"
|
||||
}}
|
||||
size={SIZE.xxl}
|
||||
>
|
||||
Use promo code{" "}
|
||||
<Text
|
||||
{Platform.OS === "android"
|
||||
? (product.data as RNIap.SubscriptionAndroid)
|
||||
?.subscriptionOfferDetails[0].pricingPhases
|
||||
.pricingPhaseList?.[0]?.formattedPrice
|
||||
: (product.data as RNIap.SubscriptionIOS)
|
||||
?.introductoryPrice ||
|
||||
(product.data as RNIap.SubscriptionIOS)
|
||||
?.localizedPrice}{" "}
|
||||
{product?.cycleText
|
||||
? `for ${product.cycleText}`
|
||||
: product?.offerType}
|
||||
</Heading>
|
||||
{product?.cycleText ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
fontFamily: "OpenSans-SemiBold"
|
||||
color: colors.secondary.paragraph,
|
||||
alignSelf: "center",
|
||||
textAlign: "center"
|
||||
}}
|
||||
size={SIZE.md}
|
||||
>
|
||||
{promo.promoCode}
|
||||
</Text>{" "}
|
||||
at checkout
|
||||
</Paragraph>
|
||||
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
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onPress={() => {
|
||||
if (!product?.data) return;
|
||||
buySubscription(product.data);
|
||||
}}
|
||||
height={40}
|
||||
width="50%"
|
||||
type="accent"
|
||||
title="Subscribe now"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
</>
|
||||
<Button
|
||||
onPress={() => {
|
||||
setProduct(undefined);
|
||||
}}
|
||||
style={{
|
||||
marginTop: 5
|
||||
}}
|
||||
height={30}
|
||||
fontSize={13}
|
||||
type="errorShade"
|
||||
title="Cancel promo code"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!user ? (
|
||||
{!user || !upgrade ? (
|
||||
<Paragraph
|
||||
color={colors.secondary.paragraph}
|
||||
size={SIZE.xs}
|
||||
@@ -355,7 +645,7 @@ export const PricingPlans = ({
|
||||
</Paragraph>
|
||||
) : null}
|
||||
|
||||
{user ? (
|
||||
{user && upgrade ? (
|
||||
<>
|
||||
{Platform.OS === "ios" ? (
|
||||
<Paragraph
|
||||
|
||||
@@ -48,7 +48,7 @@ export const ProTag = ({ width, size, background }) => {
|
||||
marginRight: 3
|
||||
}}
|
||||
size={size}
|
||||
color={colors.static.orange}
|
||||
color={colors.primary.accent}
|
||||
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";
|
||||
import { Color, Note } from "@notesnook/core/dist/types";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import React from "react";
|
||||
import {
|
||||
Platform,
|
||||
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";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useEffect } from "react";
|
||||
import { 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 { Debug } from "@notesnook/core";
|
||||
import { Debug } from "@notesnook/core/dist/api/debug";
|
||||
import { getModel, getBrand, getSystemVersion } from "react-native-device-info";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
@@ -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";
|
||||
import type { LinkAttributes } from "@notesnook/editor/dist/extensions/link";
|
||||
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 { sanitizeTag } from "@notesnook/core";
|
||||
import { Tag } from "@notesnook/core";
|
||||
import { Tags } from "@notesnook/core/dist/collections/tags";
|
||||
import { Tag } from "@notesnook/core/dist/types";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, {
|
||||
RefObject,
|
||||
@@ -268,8 +268,8 @@ const ManageTagsSheet = (props: {
|
||||
fwdRef={inputRef}
|
||||
autoCapitalize="none"
|
||||
onChangeText={(v) => {
|
||||
setQuery(sanitizeTag(v));
|
||||
checkQueryExists(sanitizeTag(v));
|
||||
setQuery(Tags.sanitize(v));
|
||||
checkQueryExists(Tags.sanitize(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";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { Platform, View } from "react-native";
|
||||
@@ -95,9 +95,9 @@ export default function Migrate() {
|
||||
await sleep(1);
|
||||
const { error, report } = await BackupService.run(false, "local");
|
||||
if (error) {
|
||||
ToastManager.error(error as Error, "Backup failed");
|
||||
ToastManager.error(error, "Backup failed");
|
||||
if (report) {
|
||||
reportError(error as Error);
|
||||
reportError(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";
|
||||
import { Note, Notebook } from "@notesnook/core/dist/types";
|
||||
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";
|
||||
import { ContentBlock, ItemReference, Note } from "@notesnook/core/dist/types";
|
||||
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";
|
||||
import { Item, ItemReference } from "@notesnook/core/dist/types";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { RefObject, useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
600
apps/mobile/app/components/sheets/restore-data/index.js
Normal file
600
apps/mobile/app/components/sheets/restore-data/index.js
Normal file
@@ -0,0 +1,600 @@
|
||||
/*
|
||||
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,7 +42,6 @@ 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 };
|
||||
@@ -86,9 +85,7 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
);
|
||||
const translateX = useSharedValue(
|
||||
widths
|
||||
? appState &&
|
||||
appState?.movedAway === false &&
|
||||
useTabStore.getState().getCurrentNoteId()
|
||||
? appState && !appState?.movedAway
|
||||
? widths.sidebar + widths.list
|
||||
: widths.sidebar
|
||||
: 0
|
||||
@@ -124,9 +121,9 @@ export const FluidTabs = forwardRef<TabsRef, TabProps>(function FluidTabs(
|
||||
} else {
|
||||
if (prevWidths.current?.sidebar !== widths.sidebar) {
|
||||
translateX.value =
|
||||
appState && appState?.movedAway === false
|
||||
? editorPosition
|
||||
: widths.sidebar;
|
||||
!appState || (appState && appState?.movedAway)
|
||||
? widths.sidebar
|
||||
: editorPosition;
|
||||
if (translateX.value === editorPosition) {
|
||||
onChangeTab?.({ i: 2, from: 1 });
|
||||
}
|
||||
|
||||
235
apps/mobile/app/components/toast/index.js
Normal file
235
apps/mobile/app/components/toast/index.js
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
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>
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -1,219 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2023 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React, { 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;
|
||||
};
|
||||
@@ -112,7 +112,7 @@ export const Button = ({
|
||||
NativeTooltip.show(event, tooltipText, NativeTooltip.POSITIONS.TOP);
|
||||
}
|
||||
}}
|
||||
disabled={loading || restProps.disabled}
|
||||
disabled={loading}
|
||||
type={type}
|
||||
accentColor={accentColor}
|
||||
accentText={accentText}
|
||||
@@ -132,7 +132,6 @@ export const Button = ({
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
opacity: restProps?.disabled ? 0.5 : 1,
|
||||
...(style as ViewStyle)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -41,7 +41,6 @@ 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>;
|
||||
@@ -50,8 +49,7 @@ interface InputProps extends TextInputProps {
|
||||
| "email"
|
||||
| "confirmPassword"
|
||||
| "username"
|
||||
| "phonenumber"
|
||||
| "url";
|
||||
| "phonenumber";
|
||||
loading?: boolean;
|
||||
onSubmit?: (
|
||||
event: NativeSyntheticEvent<TextInputSubmitEditingEventData>
|
||||
@@ -153,9 +151,6 @@ const Input = ({
|
||||
case "confirmPassword":
|
||||
isError = customValidator && value === customValidator();
|
||||
break;
|
||||
case "url":
|
||||
isError = isURL(value);
|
||||
break;
|
||||
case "phonenumber": {
|
||||
const result = phone(value, {
|
||||
strictDetection: true,
|
||||
|
||||
@@ -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";
|
||||
import { isReminderActive } from "@notesnook/core/dist/collections/reminders";
|
||||
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";
|
||||
import { Reminder } from "@notesnook/core/dist/types";
|
||||
|
||||
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";
|
||||
import { VAULT_ERRORS } from "@notesnook/core/dist/api/vault";
|
||||
import {
|
||||
Color,
|
||||
ItemReference,
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
Reminder,
|
||||
Tag,
|
||||
TrashItem
|
||||
} from "@notesnook/core";
|
||||
} from "@notesnook/core/dist/types";
|
||||
import { DisplayedNotification } from "@notifee/react-native";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
@@ -18,10 +18,18 @@ 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";
|
||||
import { EventManagerSubscription } 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 notifee from "@notifee/react-native";
|
||||
import NetInfo, { NetInfoSubscription } from "@react-native-community/netinfo";
|
||||
import NetInfo, {
|
||||
NetInfoState,
|
||||
NetInfoSubscription
|
||||
} from "@react-native-community/netinfo";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
AppState,
|
||||
@@ -39,7 +47,6 @@ 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";
|
||||
@@ -90,8 +97,7 @@ 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";
|
||||
import { initializeLogger } from "../common/database/logger";
|
||||
|
||||
const onCheckSyncStatus = async (type: SyncStatusEvent) => {
|
||||
const { disableSync, disableAutoSync } = SettingsService.get();
|
||||
@@ -251,9 +257,9 @@ async function saveEditorState() {
|
||||
movedAway: editorState().movedAway,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
NotesnookModule.setAppState(state);
|
||||
MMKV.setString("appState", state);
|
||||
} else {
|
||||
NotesnookModule.setAppState("");
|
||||
MMKV.removeItem("appState");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,14 +357,11 @@ const IsDatabaseMigrationRequired = () => {
|
||||
const initializeDatabase = async (password?: string) => {
|
||||
if (useUserStore.getState().appLocked) return;
|
||||
if (!db.isInitialized) {
|
||||
RNBootSplash.hide({ fade: false });
|
||||
changeSystemBarColors();
|
||||
|
||||
RNBootSplash.hide({ fade: true });
|
||||
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");
|
||||
@@ -388,15 +391,18 @@ 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;
|
||||
}>
|
||||
>({});
|
||||
|
||||
@@ -459,31 +465,14 @@ export const useAppEvents = () => {
|
||||
const user = await db.user.getUser();
|
||||
if (PremiumService.get() && user) {
|
||||
if (
|
||||
SettingsService.get().backupDirectoryAndroid ||
|
||||
Platform.OS !== "android"
|
||||
await BackupService.checkBackupRequired(SettingsService.get().reminder)
|
||||
) {
|
||||
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);
|
||||
if (
|
||||
!SettingsService.get().backupDirectoryAndroid &&
|
||||
Platform.OS === "android"
|
||||
)
|
||||
return;
|
||||
sleep(2000).then(() => BackupService.run());
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
@@ -544,6 +533,7 @@ export const useAppEvents = () => {
|
||||
setRecoveryKeyMessage();
|
||||
}
|
||||
if (!user?.isEmailConfirmed) setEmailVerifyMessage();
|
||||
refValues.current.isUserReady = true;
|
||||
|
||||
syncedOnLaunch.current = true;
|
||||
if (!isLogin) {
|
||||
@@ -617,21 +607,20 @@ export const useAppEvents = () => {
|
||||
}, [onSyncComplete, onUserUpdated]);
|
||||
|
||||
useEffect(() => {
|
||||
const onInternetStateChanged = async () => {
|
||||
const onInternetStateChanged = async (state: NetInfoState) => {
|
||||
if (!syncedOnLaunch.current) return;
|
||||
Sync.run("global", false, "full");
|
||||
reconnectSSE();
|
||||
reconnectSSE(state);
|
||||
};
|
||||
|
||||
const onAppStateChanged = async (state: AppStateStatus) => {
|
||||
if (state === "active") {
|
||||
notifee.setBadgeCount(0);
|
||||
updateStatusBarColor();
|
||||
|
||||
checkAutoBackup();
|
||||
Sync.run("global", false, "full");
|
||||
reconnectSSE();
|
||||
await reconnectSSE();
|
||||
await checkForShareExtensionLaunchedInBackground();
|
||||
NotesnookModule.setAppState("");
|
||||
MMKV.removeItem("appState");
|
||||
let user = await db.user.getUser();
|
||||
if (user && !user?.isEmailConfirmed) {
|
||||
try {
|
||||
@@ -715,15 +704,42 @@ export const useAppEvents = () => {
|
||||
}
|
||||
}, [appLocked, syncing, checkAutoBackup]);
|
||||
|
||||
async function reconnectSSE() {
|
||||
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;
|
||||
try {
|
||||
if (refValues.current?.isConnectingSSE) return;
|
||||
refValues.current.isConnectingSSE = true;
|
||||
await db.connectSSE();
|
||||
refValues.current.isConnectingSSE = false;
|
||||
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;
|
||||
} catch (e) {
|
||||
refValues.current.isConnectingSSE = false;
|
||||
DatabaseLogger.error(e as Error);
|
||||
refValues.current.isReconnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,817 +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 { 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";
|
||||
import { Product } from "@notesnook/core/dist/api/pricing";
|
||||
|
||||
const skuInfos: { [name: string]: Product | undefined } = {};
|
||||
|
||||
@@ -49,9 +49,7 @@ export const usePricing = (period: "monthly" | "yearly") => {
|
||||
));
|
||||
skuInfos[period] = skuInfo;
|
||||
|
||||
const products = (await (
|
||||
await PremiumService.loadProductsAndSubs()
|
||||
).subs) as Subscription[];
|
||||
const products = (await PremiumService.getProducts()) 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";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { db } from "../common/database";
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ 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,
|
||||
@@ -73,7 +74,7 @@ import { sleep } from "../utils/time";
|
||||
import { NavigationStack } from "./navigation-stack";
|
||||
|
||||
const _TabsHolder = () => {
|
||||
const { colors } = useThemeColors();
|
||||
const { colors, isDark } = useThemeColors();
|
||||
const deviceMode = useSettingStore((state) => state.deviceMode);
|
||||
const setFullscreen = useSettingStore((state) => state.setFullscreen);
|
||||
const fullscreen = useSettingStore((state) => state.fullscreen);
|
||||
@@ -269,29 +270,33 @@ const _TabsHolder = () => {
|
||||
}
|
||||
|
||||
const state = getAppState();
|
||||
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;
|
||||
}
|
||||
|
||||
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()) ||
|
||||
editorState().movedAway === false
|
||||
) {
|
||||
tabBarRef.current?.goToIndex(2, false);
|
||||
} else {
|
||||
tabBarRef.current?.goToIndex(1, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
const onScroll = (scrollOffset) => {
|
||||
@@ -381,12 +386,18 @@ const _TabsHolder = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// useEffect(() => {
|
||||
// changeSystemBarColors();
|
||||
// setTimeout(() => {
|
||||
// changeSystemBarColors();
|
||||
// }, 1000);
|
||||
// }, [colors.primary.background, isDark]);
|
||||
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]);
|
||||
|
||||
return (
|
||||
<View
|
||||
|
||||
@@ -36,13 +36,12 @@
|
||||
"react-native-wheel-color-picker": "^1.3.1",
|
||||
"tinycolor2": "1.6.0",
|
||||
"@azure/core-asynciterator-polyfill": "^1.0.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"
|
||||
"@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"
|
||||
},
|
||||
"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";
|
||||
import type { Attachment } from "@notesnook/editor";
|
||||
import type { ImageAttributes } from "@notesnook/editor";
|
||||
import type { LinkAttributes } from "@notesnook/editor";
|
||||
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 { createRef, RefObject } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { EdgeInsets } from "react-native-safe-area-context";
|
||||
|
||||
@@ -18,8 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Sodium from "@ammarahmed/react-native-sodium";
|
||||
import { isImage } from "@notesnook/core";
|
||||
import { basename } from "pathe";
|
||||
import { isImage } from "@notesnook/core/dist/utils/filename";
|
||||
import { Platform } from "react-native";
|
||||
import RNFetchBlob from "react-native-blob-util";
|
||||
import DocumentPicker, {
|
||||
@@ -42,6 +41,7 @@ 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
|
||||
))
|
||||
) {
|
||||
throw new Error("Failed to attach file");
|
||||
}
|
||||
)
|
||||
return;
|
||||
if (Platform.OS === "ios") await RNFetchBlob.fs.unlink(uri);
|
||||
|
||||
if (!fileOptions.tabId) return;
|
||||
if (
|
||||
fileOptions.tabId !== undefined &&
|
||||
fileOptions.tabId &&
|
||||
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");
|
||||
}
|
||||
|
||||
eSendEvent(eCloseSheet);
|
||||
setTimeout(() => {
|
||||
eSendEvent(eCloseSheet);
|
||||
}, 1000);
|
||||
} catch (e) {
|
||||
eSendEvent(eCloseSheet);
|
||||
ToastManager.show({
|
||||
heading: "Failed to attach file",
|
||||
message: (e as Error).message,
|
||||
heading: (e as Error).message,
|
||||
message: "You need internet access to attach a file",
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
@@ -203,6 +203,7 @@ const camera = async (options: PickerOptions) => {
|
||||
type: "error",
|
||||
context: "global"
|
||||
});
|
||||
console.log("attachment error:", e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ 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";
|
||||
import type { ToolbarGroupDefinition } from "@notesnook/editor/dist/toolbar/types";
|
||||
import { useEditor } from "./use-editor";
|
||||
import { Note } from "@notesnook/core/dist/types";
|
||||
export type useEditorType = ReturnType<typeof useEditor>;
|
||||
|
||||
export type EditorState = {
|
||||
@@ -90,6 +91,7 @@ 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";
|
||||
import type { Attachment } from "@notesnook/editor";
|
||||
import { ItemReference } from "@notesnook/core/dist/types";
|
||||
import type { Attachment } from "@notesnook/editor/dist/extensions/attachment/index";
|
||||
import { getDefaultPresets } from "@notesnook/editor/dist/toolbar/tool-definitions";
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import React, { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
@@ -18,10 +18,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getFormattedDate } from "@notesnook/common";
|
||||
import { isEncryptedContent } from "@notesnook/core/dist/collections/content";
|
||||
import { NoteContent } from "@notesnook/core/dist/collections/session-content";
|
||||
import { EVENTS } from "@notesnook/core/dist/common";
|
||||
import {
|
||||
EVENTS,
|
||||
isEncryptedContent,
|
||||
NoteContent,
|
||||
ContentItem,
|
||||
ContentType,
|
||||
DeletedItem,
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
UnencryptedContentItem,
|
||||
isDeleted,
|
||||
isTrashItem
|
||||
} from "@notesnook/core";
|
||||
} from "@notesnook/core/dist/types";
|
||||
import { useThemeEngineStore } from "@notesnook/theme";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import WebView from "react-native-webview";
|
||||
@@ -660,9 +660,9 @@ export const useEditor = (
|
||||
lock.current = true;
|
||||
|
||||
// Handle this case where note was locked on another device and synced.
|
||||
const locked = note
|
||||
? await db.vaults.itemExists(note as ItemReference)
|
||||
: false;
|
||||
const locked = await db.vaults.itemExists(
|
||||
currentNotes.current[noteId] as ItemReference
|
||||
);
|
||||
|
||||
if (note) {
|
||||
if (!locked && tab?.noteLocked) {
|
||||
|
||||
@@ -17,20 +17,20 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { parseInternalLink } from "@notesnook/core";
|
||||
import { createRef, MutableRefObject, RefObject } from "react";
|
||||
import { TextInput } from "react-native";
|
||||
import WebView from "react-native-webview";
|
||||
import { db } from "../../../common/database";
|
||||
import { MMKV } from "../../../common/database/mmkv";
|
||||
import {
|
||||
eSendEvent,
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../../services/event-manager";
|
||||
import { eOnLoadNote } from "../../../utils/events";
|
||||
import { NotesnookModule } from "../../../utils/notesnook-module";
|
||||
import { AppState, EditorState, useEditorType } from "./types";
|
||||
import { useTabStore } from "./use-tab-store";
|
||||
import { parseInternalLink } from "@notesnook/core";
|
||||
import { eOnLoadNote } from "../../../utils/events";
|
||||
import { db } from "../../../common/database";
|
||||
export const textInput = createRef<TextInput>();
|
||||
export const editorController =
|
||||
createRef<useEditorType>() as MutableRefObject<useEditorType>;
|
||||
@@ -154,13 +154,18 @@ export function isContentInvalid(content: string | undefined) {
|
||||
}
|
||||
|
||||
const canRestoreAppState = (appState: AppState) => {
|
||||
return appState.editing && Date.now() < appState.timestamp + 3600000;
|
||||
return (
|
||||
appState.editing &&
|
||||
!appState.note?.locked &&
|
||||
appState.note?.id &&
|
||||
Date.now() < appState.timestamp + 3600000
|
||||
);
|
||||
};
|
||||
|
||||
let appState: AppState | undefined;
|
||||
export function getAppState() {
|
||||
if (appState && canRestoreAppState(appState)) return appState as AppState;
|
||||
const json = NotesnookModule.getAppState();
|
||||
const json = MMKV.getString("appState");
|
||||
if (json) {
|
||||
appState = JSON.parse(json) as AppState;
|
||||
if (canRestoreAppState(appState)) {
|
||||
@@ -175,7 +180,7 @@ export function getAppState() {
|
||||
|
||||
export function clearAppState() {
|
||||
appState = undefined;
|
||||
NotesnookModule.setAppState("");
|
||||
MMKV.removeItem("appState");
|
||||
}
|
||||
|
||||
export async function openInternalLink(url: string) {
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { resolveItems } from "@notesnook/common";
|
||||
import { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Note, Notebook } from "@notesnook/core";
|
||||
import { Note, Notebook } from "@notesnook/core/dist/types";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { db } from "../../common/database";
|
||||
|
||||
@@ -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 } from "@notesnook/core";
|
||||
import { Color } from "@notesnook/core/dist/types";
|
||||
import React from "react";
|
||||
import NotesPage from ".";
|
||||
import { db } from "../../common/database";
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import { resolveItems } from "@notesnook/common";
|
||||
import { VirtualizedGrouping } from "@notesnook/core";
|
||||
import { Color, Note } from "@notesnook/core";
|
||||
import { Color, Note } from "@notesnook/core/dist/types";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { db } from "../../common/database";
|
||||
import { FloatingButton } from "../../components/container/floating-button";
|
||||
|
||||
@@ -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 { Tag } from "@notesnook/core";
|
||||
import { Tag } from "@notesnook/core/dist/types";
|
||||
import React from "react";
|
||||
import NotesPage from ".";
|
||||
import { db } from "../../common/database";
|
||||
|
||||
@@ -32,7 +32,7 @@ import { NavigationProps } from "../../services/navigation";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { eOnRefreshSearch } from "../../utils/events";
|
||||
import { SearchBar } from "./search-bar";
|
||||
import { FilteredSelector } from "@notesnook/core";
|
||||
import { FilteredSelector } from "@notesnook/core/dist/database/sql-collection";
|
||||
export const Search = ({ route, navigation }: NavigationProps<"Search">) => {
|
||||
const [results, setResults] = useState<VirtualizedGrouping<Item>>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
@@ -1,123 +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 { formatBytes } from "@notesnook/common";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import React from "react";
|
||||
import { View } from "react-native";
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import { db } from "../../common/database";
|
||||
import { IconButton } from "../../components/ui/icon-button";
|
||||
import { ProgressBarComponent } from "../../components/ui/svg/lazy";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { useAttachmentProgress } from "../../hooks/use-attachment-progress";
|
||||
import { useDBItem } from "../../hooks/use-db-item";
|
||||
import { useAttachmentStore } from "../../stores/use-attachment-store";
|
||||
import { SIZE } from "../../utils/size";
|
||||
|
||||
export const AttachmentGroupProgress = (props: { groupId?: string }) => {
|
||||
const { colors } = useThemeColors();
|
||||
const progress = useAttachmentStore((state) =>
|
||||
!props.groupId ? undefined : state.downloading?.[props.groupId]
|
||||
);
|
||||
const [file] = useDBItem(progress?.filename, "attachment");
|
||||
const [fileProgress] = useAttachmentProgress(file, false);
|
||||
|
||||
return !progress ||
|
||||
progress.canceled ||
|
||||
progress.current === progress.total ? null : (
|
||||
<View
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary.border,
|
||||
borderRadius: 10,
|
||||
padding: 12,
|
||||
flexDirection: "row",
|
||||
gap: 10
|
||||
}}
|
||||
>
|
||||
<Icon name="download" size={SIZE.xxxl} />
|
||||
<View
|
||||
style={{
|
||||
gap: 5,
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
<Paragraph>
|
||||
{progress.message || "Downloading files"} ({progress?.current}/
|
||||
{progress?.total})
|
||||
</Paragraph>
|
||||
|
||||
{progress && progress.current && progress.total ? (
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
marginTop: 10
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={null}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
progress={progress.current / progress.total}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<Paragraph
|
||||
style={{
|
||||
fontSize: 10,
|
||||
color: colors.secondary.paragraph
|
||||
}}
|
||||
numberOfLines={1}
|
||||
>
|
||||
Downloading {file?.filename} {formatBytes(file?.size || 0)}{" "}
|
||||
{fileProgress?.percent ? `(${fileProgress.percent})` : ""}
|
||||
</Paragraph>
|
||||
<Paragraph size={10} color={colors.secondary.paragraph}>
|
||||
Group: {props.groupId}
|
||||
</Paragraph>
|
||||
</View>
|
||||
{props.groupId === "offline-mode" ? null : (
|
||||
<IconButton
|
||||
name="close"
|
||||
onPress={() => {
|
||||
if (props.groupId) {
|
||||
useAttachmentStore.getState().setDownloading({
|
||||
groupId: props.groupId,
|
||||
canceled: true,
|
||||
current: 0,
|
||||
total: 0,
|
||||
message: undefined
|
||||
});
|
||||
setTimeout(() => {
|
||||
if (props.groupId) {
|
||||
db.fs().cancel(props.groupId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -18,29 +18,23 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, { ReactElement } from "react";
|
||||
import { AttachmentDialog } from "../../components/attachments";
|
||||
import { AccentColorPicker } from "./appearance";
|
||||
import DebugLogs from "./debug";
|
||||
import { ConfigureToolbar } from "./editor/configure-toolbar";
|
||||
import { Licenses } from "./licenses";
|
||||
import { AttachmentGroupProgress } from "./attachment-group-progress";
|
||||
import {
|
||||
ApplockTimerPicker,
|
||||
BackupReminderPicker,
|
||||
BackupWithAttachmentsReminderPicker,
|
||||
DateFormatPicker,
|
||||
FontPicker,
|
||||
HomePicker,
|
||||
TimeFormatPicker,
|
||||
TrashIntervalPicker
|
||||
} from "./picker/pickers";
|
||||
import { RestoreBackup } from "./restore-backup";
|
||||
import { ServersConfiguration } from "./server-config";
|
||||
import SoundPicker from "./sound-picker";
|
||||
import { Subscription } from "./subscription";
|
||||
import ThemeSelector from "./theme-selector";
|
||||
import { TitleFormat } from "./title-format";
|
||||
import { View } from "react-native";
|
||||
import {
|
||||
HomePicker,
|
||||
DateFormatPicker,
|
||||
FontPicker,
|
||||
TimeFormatPicker,
|
||||
TrashIntervalPicker,
|
||||
BackupReminderPicker,
|
||||
ApplockTimerPicker
|
||||
} from "./picker/pickers";
|
||||
import ThemeSelector from "./theme-selector";
|
||||
|
||||
export const components: { [name: string]: ReactElement } = {
|
||||
colorpicker: <AccentColorPicker />,
|
||||
@@ -57,14 +51,5 @@ export const components: { [name: string]: ReactElement } = {
|
||||
"date-format-selector": <DateFormatPicker />,
|
||||
"time-format-selector": <TimeFormatPicker />,
|
||||
"theme-selector": <ThemeSelector />,
|
||||
"applock-timer": <ApplockTimerPicker />,
|
||||
autobackupsattachments: <BackupWithAttachmentsReminderPicker />,
|
||||
backuprestore: <RestoreBackup />,
|
||||
"server-config": <ServersConfiguration />,
|
||||
"attachments-manager": <AttachmentDialog note={undefined} isSheet={false} />,
|
||||
"offline-mode-progress": (
|
||||
<View style={{ paddingHorizontal: 12 }}>
|
||||
<AttachmentGroupProgress groupId="offline-mode" />
|
||||
</View>
|
||||
)
|
||||
"applock-timer": <ApplockTimerPicker />
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import Clipboard from "@react-native-clipboard/clipboard";
|
||||
import { LogMessage } from "@notesnook/logger";
|
||||
import { format, LogLevel, logManager } from "@notesnook/core";
|
||||
import { format, LogLevel, logManager } from "@notesnook/core/dist/logger";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { FlatList, Platform, TouchableOpacity, View } from "react-native";
|
||||
import * as ScopedStorage from "react-native-scoped-storage";
|
||||
|
||||
@@ -32,7 +32,7 @@ import { DraggableItem, useDragState } from "./state";
|
||||
import ToolSheet from "./tool-sheet";
|
||||
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import type { ToolId } from "@notesnook/editor";
|
||||
import { ToolId } from "@notesnook/editor/dist/toolbar/tools";
|
||||
import PremiumService from "../../../services/premium";
|
||||
|
||||
export const Group = ({
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { CURRENT_TOOLBAR_VERSION, migrateToolbar } from "@notesnook/common";
|
||||
import type { ToolbarGroupDefinition } from "@notesnook/editor";
|
||||
import type { ToolbarGroupDefinition } from "@notesnook/editor/dist/toolbar/types";
|
||||
import create, { State } from "zustand";
|
||||
import { StateStorage, persist } from "zustand/middleware";
|
||||
import { db } from "../../../common/database";
|
||||
|
||||
@@ -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 type { ToolId } from "@notesnook/editor";
|
||||
import { ToolId } from "@notesnook/editor/dist/toolbar";
|
||||
import React, { RefObject } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Pressable } from "../../../components/ui/pressable";
|
||||
|
||||
@@ -34,7 +34,7 @@ import { findToolById, getToolIcon } from "./toolbar-definition";
|
||||
import ToolSheet from "./tool-sheet";
|
||||
|
||||
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
|
||||
import type { ToolId } from "@notesnook/editor";
|
||||
import { ToolId } from "@notesnook/editor/dist/toolbar/tools";
|
||||
import PremiumService from "../../../services/premium";
|
||||
|
||||
export const Tool = ({
|
||||
|
||||
@@ -18,12 +18,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Icons } from "@notesnook/editor/dist/toolbar/icons";
|
||||
import { ToolbarGroupDefinition } from "@notesnook/editor";
|
||||
import { ToolbarGroupDefinition } from "@notesnook/editor/dist/toolbar/index";
|
||||
import {
|
||||
getAllTools,
|
||||
getDefaultPresets
|
||||
} from "@notesnook/editor/dist/toolbar/tool-definitions";
|
||||
import { ToolId } from "@notesnook/editor";
|
||||
import { ToolId } from "@notesnook/editor/dist/toolbar/tools";
|
||||
|
||||
export const tools = getAllTools();
|
||||
export const presets: { [name: string]: ToolbarGroupDefinition[] } = {
|
||||
|
||||
@@ -23,13 +23,13 @@ import { View } from "react-native";
|
||||
import { KeyboardAwareFlatList } from "react-native-keyboard-aware-scroll-view";
|
||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||
import DelayLayout from "../../components/delay-layout";
|
||||
import { Header } from "../../components/header";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { tabBarRef } from "../../utils/global-refs";
|
||||
import { components } from "./components";
|
||||
import { SectionItem } from "./section-item";
|
||||
import { RouteParams, SettingSection } from "./types";
|
||||
import { Header } from "../../components/header";
|
||||
|
||||
const keyExtractor = (item: SettingSection) => item.id;
|
||||
const AnimatedKeyboardAvoidingFlatList = Animated.createAnimatedComponent(
|
||||
@@ -58,21 +58,18 @@ const Group = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{route.params.hideHeader ? null : (
|
||||
<Header
|
||||
renderedInRoute="Settings"
|
||||
title={route.params.name as string}
|
||||
canGoBack={true}
|
||||
id="Settings"
|
||||
/>
|
||||
)}
|
||||
<Header
|
||||
renderedInRoute="Settings"
|
||||
title={route.params.name as string}
|
||||
canGoBack={true}
|
||||
id="Settings"
|
||||
/>
|
||||
<DelayLayout type="settings" delay={1}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1
|
||||
}}
|
||||
>
|
||||
{route.params.component ? components[route.params.component] : null}
|
||||
{route.params.sections ? (
|
||||
<AnimatedKeyboardAvoidingFlatList
|
||||
entering={FadeInDown}
|
||||
@@ -83,6 +80,7 @@ const Group = ({
|
||||
enableAutomaticScroll
|
||||
/>
|
||||
) : null}
|
||||
{route.params.component ? components[route.params.component] : null}
|
||||
</View>
|
||||
</DelayLayout>
|
||||
</>
|
||||
|
||||
@@ -18,22 +18,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { NativeStackScreenProps } from "@react-navigation/native-stack";
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Animated, { FadeInDown } from "react-native-reanimated";
|
||||
import DelayLayout from "../../components/delay-layout";
|
||||
import { Header } from "../../components/header";
|
||||
import BaseDialog from "../../components/dialog/base-dialog";
|
||||
import { ProgressBarComponent } from "../../components/ui/svg/lazy";
|
||||
import Heading from "../../components/ui/typography/heading";
|
||||
import Paragraph from "../../components/ui/typography/paragraph";
|
||||
import { useNavigationFocus } from "../../hooks/use-navigation-focus";
|
||||
import {
|
||||
eSubscribeEvent,
|
||||
eUnSubscribeEvent
|
||||
} from "../../services/event-manager";
|
||||
import useNavigationStore from "../../stores/use-navigation-store";
|
||||
import { useThemeColors } from "@notesnook/theme";
|
||||
import { SIZE } from "../../utils/size";
|
||||
import { SectionGroup } from "./section-group";
|
||||
import { settingsGroups } from "./settings-data";
|
||||
import { RouteParams, SettingSection } from "./types";
|
||||
import SettingsUserSection from "./user-section";
|
||||
import { Header } from "../../components/header";
|
||||
const keyExtractor = (item: SettingSection) => item.id;
|
||||
|
||||
const Home = ({
|
||||
navigation
|
||||
}: NativeStackScreenProps<RouteParams, "SettingsHome">) => {
|
||||
const { colors } = useThemeColors();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useNavigationFocus(navigation, {
|
||||
onFocus: () => {
|
||||
useNavigationStore.getState().setFocusedRouteId("Settings");
|
||||
@@ -49,6 +62,16 @@ const Home = ({
|
||||
<SectionGroup item={item} />
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function settingsLoading(state: boolean) {
|
||||
setLoading(state || true);
|
||||
}
|
||||
eSubscribeEvent("settings-loading", settingsLoading);
|
||||
return () => {
|
||||
eUnSubscribeEvent("settings-loading", settingsLoading);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
@@ -58,6 +81,57 @@ const Home = ({
|
||||
id="Settings"
|
||||
/>
|
||||
<DelayLayout delay={300} type="settings">
|
||||
{loading && (
|
||||
//@ts-ignore // Migrate to typescript required.
|
||||
<BaseDialog animated={false} bounce={false} visible={true}>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: colors.primary.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
<Heading color={colors.primary.paragraph} size={SIZE.lg}>
|
||||
Logging out
|
||||
</Heading>
|
||||
<Paragraph color={colors.secondary.paragraph}>
|
||||
Please wait while we log out and clear app data.
|
||||
</Paragraph>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
width: 100,
|
||||
marginTop: 15
|
||||
}}
|
||||
>
|
||||
<ProgressBarComponent
|
||||
height={5}
|
||||
width={100}
|
||||
animated={true}
|
||||
useNativeDriver
|
||||
indeterminate
|
||||
indeterminateAnimationDuration={2000}
|
||||
unfilledColor={colors.secondary.background}
|
||||
color={colors.primary.accent}
|
||||
borderWidth={0}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</BaseDialog>
|
||||
)}
|
||||
|
||||
<Animated.FlatList
|
||||
entering={FadeInDown}
|
||||
data={settingsGroups}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user