diff --git a/.github/workflows/build-early.yml b/.github/workflows/build-early.yml index c2d99ee8e..6498498c6 100644 --- a/.github/workflows/build-early.yml +++ b/.github/workflows/build-early.yml @@ -54,6 +54,16 @@ jobs: with: node-version: ${{ matrix.node }} + - name: Install Protoc + run: | + PB_REL="https://github.com/protocolbuffers/protobuf/releases" + curl -LO $PB_REL/download/v22.2/protoc-22.2-linux-x86_64.zip + unzip protoc-22.2-linux-x86_64.zip -d $HOME/.local + export PATH="$PATH:$HOME/.local/bin" + + - name: Check protoc installation + run: protoc --version + - name: Setup pnpm store path run: pnpm config set store-dir ~/.pnpm-store @@ -66,7 +76,7 @@ jobs: ${{ runner.os }}-pnpm-store- - name: pnpm install - run: pnpm install + run: pnpm inheritance-install - name: Setup turbo cache env: @@ -115,6 +125,16 @@ jobs: with: node-version: ${{ matrix.node }} + - name: Install Protoc + run: | + PB_REL="https://github.com/protocolbuffers/protobuf/releases" + curl -LO $PB_REL/download/v22.2/protoc-22.2-linux-x86_64.zip + unzip protoc-22.2-linux-x86_64.zip -d $HOME/.local + export PATH="$PATH:$HOME/.local/bin" + + - name: Check protoc installation + run: protoc --version + - name: Setup pnpm store path run: pnpm config set store-dir ~/.pnpm-store @@ -130,7 +150,7 @@ jobs: run: sudo apt-get install fakeroot dpkg rpm -y - name: pnpm install - run: pnpm install + run: pnpm inheritance-install - name: Setup turbo cache env: @@ -205,11 +225,11 @@ jobs: - name: Fix hashBang in pnpm.cjs env: WINDOWS_USERNAME: ${{ secrets.WINDOWS_USERNAME }} - IS_CI: "true" + IS_CI: 'true' run: pnpm fix:hashbang - name: pnpm install - run: pnpm install + run: pnpm inheritance-install - name: Setup turbo cache env: @@ -281,7 +301,7 @@ jobs: ${{ runner.os }}-pnpm-store- - name: pnpm install - run: pnpm install + run: pnpm inheritance-install - name: Setup turbo cache env: diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 43d591dd5..196962b5d 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -46,6 +46,17 @@ jobs: with: node-version: ${{ matrix.node }} + - name: Install Protoc + run: | + PB_REL="https://github.com/protocolbuffers/protobuf/releases" + curl -LO $PB_REL/download/v22.2/protoc-22.2-linux-x86_64.zip + unzip protoc-22.2-linux-x86_64.zip -d $HOME/.local + export PATH="$PATH:$HOME/.local/bin" + + - name: Check protoc installation + run: protoc --version + + - name: Setup pnpm store path run: pnpm config set store-dir ~/.pnpm-store @@ -58,7 +69,7 @@ jobs: ${{ runner.os }}-pnpm-store- - name: pnpm install - run: pnpm install + run: pnpm inheritance-install - name: Setup turbo cache env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fdf7bd015..2cf94a431 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,6 +36,17 @@ jobs: with: node-version: ${{ matrix.node }} + - name: Install Protoc + run: | + PB_REL="https://github.com/protocolbuffers/protobuf/releases" + curl -LO $PB_REL/download/v22.2/protoc-22.2-linux-x86_64.zip + unzip protoc-22.2-linux-x86_64.zip -d $HOME/.local + export PATH="$PATH:$HOME/.local/bin" + + - name: Check protoc installation + run: protoc --version + + - name: Setup pnpm store path run: pnpm config set store-dir ~/.pnpm-store @@ -48,7 +59,7 @@ jobs: ${{ runner.os }}-pnpm-store- - name: pnpm install - run: pnpm install + run: pnpm inheritance-install - name: Setup turbo cache env: diff --git a/.npmrc b/.npmrc index d0e289a7a..0a995a176 100644 --- a/.npmrc +++ b/.npmrc @@ -2,3 +2,4 @@ auto-install-peers=true # TODO: Find a better way to do this # https://github.com/electron-userland/electron-builder/issues/6289 shamefully-hoist=true +node-options=--max-old-space-size=8192 diff --git a/apps/desktop/src/main/ipc/db.ts b/apps/desktop/src/main/ipc/db.ts index 2e8d52fad..a48ed9247 100644 --- a/apps/desktop/src/main/ipc/db.ts +++ b/apps/desktop/src/main/ipc/db.ts @@ -15,6 +15,7 @@ const collectionNameList: (keyof IDatabase)[] = [ 'priceInfo', 'transactionNotificationRead', 'transactionNotificationClick', + 'inheritancePlan', ]; export const setupDbListeners = async (webContents: WebContents) => { diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts index 99e39cbba..cf69f56b4 100644 --- a/apps/desktop/src/preload/index.ts +++ b/apps/desktop/src/preload/index.ts @@ -133,6 +133,7 @@ const electronAPI = { 'priceInfo', 'transactionNotificationRead', 'transactionNotificationClick', + 'inheritancePlan', ]; const eventNames = ['change']; diff --git a/package.json b/package.json index 6d03354e5..6cee91678 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "email": "support@cypherock.com" }, "scripts": { - "start": "cross-env NODE_OPTIONS=--max-old-space-size=4096 && node scripts/watch/index.js", + "start": "cross-env NODE_OPTIONS=--max_old_space_size=8192 node scripts/watch/index.js", "dev": "turbo dev --no-daemon --filter @cypherock/cysync-desktop", "dev:cli": "bash scripts/devCli.sh", "make": "turbo make --no-daemon --filter @cypherock/cysync-desktop", @@ -32,14 +32,15 @@ "clean:force": "node scripts/clean.js -f && bash ./scripts/clean.sh", "build:submodules": "node scripts/submodules.js build", "ci:publish": "pnpm publish -r --access public --no-git-checks --filter \"@cypherock/coins\"", - "ci:version": "pnpm changeset version && pnpm i --no-frozen-lockfile", + "ci:version": "pnpm changeset version && pnpm inheritance-install --no-frozen-lockfile", "storybook": "turbo storybook --no-daemon", "build:storybook": "turbo build:storybook --no-daemon", "fix:hashbang": "bash scripts/hashBangFix.sh", "erc20-diff": "turbo run erc20-diff --no-daemon", "gen-erc20-list": "turbo run gen-erc20-list --no-daemon", "download-erc20-images": "turbo run download-erc20-images --no-daemon", - "gen-translations": "turbo run gen-translations --no-daemon" + "gen-translations": "turbo run gen-translations --no-daemon", + "inheritance-install": "node scripts/inheritanceSetup.js" }, "keywords": [], "license": "AGPL-3.0", diff --git a/packages/app-support-inheritance/.eslintrc.js b/packages/app-support-inheritance/.eslintrc.js new file mode 100644 index 000000000..0eab828b7 --- /dev/null +++ b/packages/app-support-inheritance/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + root: true, + extends: ['@cypherock/eslint-config/browser'], + parserOptions: { + tsconfigRootDir: __dirname, + project: ['./tsconfig.eslint.json'], + }, +}; diff --git a/packages/app-support-inheritance/.gitignore b/packages/app-support-inheritance/.gitignore new file mode 100644 index 000000000..49a97d4c8 --- /dev/null +++ b/packages/app-support-inheritance/.gitignore @@ -0,0 +1,3 @@ +.turbo +dist +coverage diff --git a/packages/app-support-inheritance/.prettierrc b/packages/app-support-inheritance/.prettierrc new file mode 100644 index 000000000..3add8a130 --- /dev/null +++ b/packages/app-support-inheritance/.prettierrc @@ -0,0 +1 @@ +"@cypherock/prettier-config" diff --git a/packages/app-support-inheritance/README.md b/packages/app-support-inheritance/README.md new file mode 100644 index 000000000..d93de1a54 --- /dev/null +++ b/packages/app-support-inheritance/README.md @@ -0,0 +1,3 @@ +# App Support for Inheritance + +- All coin operations related to Inheritance diff --git a/packages/app-support-inheritance/jest.config.js b/packages/app-support-inheritance/jest.config.js new file mode 100644 index 000000000..b81cbd01f --- /dev/null +++ b/packages/app-support-inheritance/jest.config.js @@ -0,0 +1,5 @@ +const baseConfig = require('@cypherock/jest-config/node'); + +module.exports = { + ...baseConfig, +}; diff --git a/packages/app-support-inheritance/package.json b/packages/app-support-inheritance/package.json new file mode 100644 index 000000000..120cd63a1 --- /dev/null +++ b/packages/app-support-inheritance/package.json @@ -0,0 +1,59 @@ +{ + "name": "@cypherock/app-support-inheritance", + "version": "0.0.0", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "types": "dist/esm/index.d.ts", + "license": "AGPL-3.0", + "private": true, + "scripts": { + "lint": "eslint --ext .ts,tsx,js,jsx src/ --fix", + "lint:check": "eslint --ext .ts,tsx,js,jsx src/", + "pretty": "prettier --write \"src/**/*.ts?(x)\" \"tests/**/*.ts?(x)\"", + "pretty:check": "prettier --check \"src/**/*.ts?(x)\" \"tests/**/*.ts?(x)\"", + "build": "rimraf dist && pnpm build:esm && pnpm build:cjs", + "build:cjs": "tsc -p tsconfig_cjs.json", + "build:esm": "tsc -p tsconfig.json", + "build:dirty": "pnpm build:esm", + "test": "jest", + "pre-commit": "lint-staged" + }, + "devDependencies": { + "@cypherock/eslint-config": "workspace:^", + "@cypherock/jest-config": "workspace:^", + "@cypherock/prettier-config": "workspace:^", + "@cypherock/tsconfig": "workspace:^", + "@jest/globals": "^29.5.0", + "@stryker-mutator/core": "^7.0.2", + "@stryker-mutator/jest-runner": "^7.0.2", + "@stryker-mutator/typescript-checker": "^7.0.2", + "@types/jest": "^29.5.2", + "@types/lodash": "^4.14.195", + "@types/node": "18.15.11", + "eslint": "^8.43.0", + "jest": "^29.5.0", + "lint-staged": "^13.2.2", + "prettier": "^2.8.8", + "rimraf": "^5.0.1", + "ts-jest": "^29.1.0", + "typescript": "^4.9.5" + }, + "dependencies": { + "@cypherock/cysync-interfaces": "workspace:^", + "@cypherock/cysync-utils": "workspace:^", + "@cypherock/sdk-app-inheritance": "^0.0.0", + "@cypherock/sdk-interfaces": "^0.0.15", + "@cypherock/sdk-utils": "^0.0.18", + "lodash": "^4.17.21", + "rxjs": "^7.8.1" + }, + "lint-staged": { + "*.{ts,tsx}": [ + "eslint --ext ts,tsx --quiet --fix --", + "prettier --write" + ], + "*.{js,jsx,md,mdx,mjs,yml,yaml,css,json}": [ + "prettier --write" + ] + } +} diff --git a/packages/app-support-inheritance/src/index.ts b/packages/app-support-inheritance/src/index.ts new file mode 100644 index 000000000..e7d1c2e82 --- /dev/null +++ b/packages/app-support-inheritance/src/index.ts @@ -0,0 +1,34 @@ +/* eslint-disable class-methods-use-this */ +import * as operations from './operations'; +import { + IInheritanceDecryptMessageParams, + IInheritanceEncryptMessageParams, + IInheritanceStartSessionParams, + IInheritanceStopSessionParams, + IInheritanceWalletAuthParams, +} from './operations/types'; + +export * from './operations/types'; +export { updateLogger } from './utils/logger'; + +export class InheritanceSupport { + public walletAuth(params: IInheritanceWalletAuthParams) { + return operations.walletAuth(params); + } + + public encryptMessageWithPin(params: IInheritanceEncryptMessageParams) { + return operations.encryptMessage(params); + } + + public decryptMessageWithPin(params: IInheritanceDecryptMessageParams) { + return operations.decryptMessage(params); + } + + public startSession(params: IInheritanceStartSessionParams) { + return operations.startSession(params); + } + + public stopSession(params: IInheritanceStopSessionParams) { + return operations.stopSession(params); + } +} diff --git a/packages/app-support-inheritance/src/operations/common.ts b/packages/app-support-inheritance/src/operations/common.ts new file mode 100644 index 000000000..be99aeb85 --- /dev/null +++ b/packages/app-support-inheritance/src/operations/common.ts @@ -0,0 +1,4 @@ +export const ENCRYPTED_DATA_SERIALIZATION_TAGS = { + WALLET_MESSAGE: 2, + NOMINEE_MESSAGE: 3, +}; diff --git a/packages/app-support-inheritance/src/operations/decryptMessage/index.ts b/packages/app-support-inheritance/src/operations/decryptMessage/index.ts new file mode 100644 index 000000000..a12e2717a --- /dev/null +++ b/packages/app-support-inheritance/src/operations/decryptMessage/index.ts @@ -0,0 +1,104 @@ +import { + DecryptMessagesWithPinEvent, + InheritanceApp, +} from '@cypherock/sdk-app-inheritance'; +import { hexToUint8Array } from '@cypherock/sdk-utils'; +import { Observable } from 'rxjs'; + +import { + IInheritanceDecryptMessageEvent, + IInheritanceDecryptMessageParams, + InheritanceDecryptMessageDeviceEvent, +} from './types'; + +import logger from '../../utils/logger'; + +const decryptMessageToDeviceEventMap: Partial< + Record< + DecryptMessagesWithPinEvent, + InheritanceDecryptMessageDeviceEvent | undefined + > +> = { + [DecryptMessagesWithPinEvent.INIT]: InheritanceDecryptMessageDeviceEvent.INIT, + [DecryptMessagesWithPinEvent.CONFIRMED]: + InheritanceDecryptMessageDeviceEvent.CONFIRMED, + [DecryptMessagesWithPinEvent.MESSAGE_DECRYPTED_CARD_TAP]: + InheritanceDecryptMessageDeviceEvent.CARD_TAPPED, + [DecryptMessagesWithPinEvent.PIN_VERIFIED]: + InheritanceDecryptMessageDeviceEvent.PIN_VERIFIED, +}; + +export const decryptMessage = ( + params: IInheritanceDecryptMessageParams, +): Observable => + new Observable(observer => { + let finished = false; + let app: InheritanceApp | undefined; + + const cleanUp = async () => { + if (app) { + try { + await app.abort(); + } catch (error) { + logger.warn('Error in aborting inheritance decryptMessage'); + logger.warn(error); + } + } + }; + + const unsubscribe = () => { + if (!finished) { + finished = true; + cleanUp(); + } + }; + + const main = async () => { + try { + const { walletId, connection, message } = params; + + const events: Record = {} as any; + + app = await InheritanceApp.create(connection); + + const decryptedMessages = await app.decryptMessagesWithPin({ + walletId: hexToUint8Array(walletId), + encryptedData: hexToUint8Array(message), + onEvent: event => { + const deviceEvent = decryptMessageToDeviceEventMap[event]; + if (deviceEvent !== undefined) { + events[deviceEvent] = true; + } + + observer.next({ + type: 'Device', + device: { isDone: false, events }, + }); + }, + }); + + observer.next({ type: 'Device', device: { isDone: true, events } }); + observer.next({ + type: 'Result', + decryptedMessages: Object.entries(decryptedMessages).reduce( + (a, [key, value]) => ({ + ...a, + [key]: value.dataAsString, + }), + {}, + ), + }); + + finished = true; + observer.complete(); + } catch (error) { + if (!finished) { + observer.error(error); + } + } + }; + + main(); + + return unsubscribe; + }); diff --git a/packages/app-support-inheritance/src/operations/decryptMessage/types.ts b/packages/app-support-inheritance/src/operations/decryptMessage/types.ts new file mode 100644 index 000000000..1f3450e5d --- /dev/null +++ b/packages/app-support-inheritance/src/operations/decryptMessage/types.ts @@ -0,0 +1,25 @@ +import { IDeviceConnection } from '@cypherock/sdk-interfaces'; + +export interface IInheritanceDecryptMessageParams { + connection: IDeviceConnection; + walletId: string; + message: string; +} + +export type IInheritanceDecryptMessageEventType = 'Result' | 'Device'; + +export enum InheritanceDecryptMessageDeviceEvent { + INIT = 0, + CONFIRMED = 1, + CARD_TAPPED = 2, + PIN_VERIFIED = 3, +} + +export interface IInheritanceDecryptMessageEvent { + type: IInheritanceDecryptMessageEventType; + decryptedMessages?: Record; + device?: { + isDone: boolean; + events: Record; + }; +} diff --git a/packages/app-support-inheritance/src/operations/encryptMessage/index.ts b/packages/app-support-inheritance/src/operations/encryptMessage/index.ts new file mode 100644 index 000000000..99d48c5d6 --- /dev/null +++ b/packages/app-support-inheritance/src/operations/encryptMessage/index.ts @@ -0,0 +1,118 @@ +import { + EncryptMessagesWithPinEvent, + InheritanceApp, + InheritanceMessage, +} from '@cypherock/sdk-app-inheritance'; +import { hexToUint8Array, uint8ArrayToHex } from '@cypherock/sdk-utils'; +import { Observable } from 'rxjs'; + +import { + IInheritanceEncryptMessageEvent, + IInheritanceEncryptMessageParams, + InheritanceEncryptMessageDeviceEvent, +} from './types'; + +import logger from '../../utils/logger'; +import { ENCRYPTED_DATA_SERIALIZATION_TAGS } from '../common'; + +const encryptMessageToDeviceEventMap: Partial< + Record< + EncryptMessagesWithPinEvent, + InheritanceEncryptMessageDeviceEvent | undefined + > +> = { + [EncryptMessagesWithPinEvent.INIT]: InheritanceEncryptMessageDeviceEvent.INIT, + [EncryptMessagesWithPinEvent.CONFIRMED]: + InheritanceEncryptMessageDeviceEvent.CONFIRMED, + [EncryptMessagesWithPinEvent.MESSAGE_VERIFIED]: + InheritanceEncryptMessageDeviceEvent.VERIFIED, + [EncryptMessagesWithPinEvent.PIN_ENTERED_CARD_TAP]: + InheritanceEncryptMessageDeviceEvent.PIN_CARD_TAPPED, + [EncryptMessagesWithPinEvent.MESSAGE_ENCRYPTED_CARD_TAP]: + InheritanceEncryptMessageDeviceEvent.MESSAGE_CARD_TAPPED, +}; + +export const encryptMessage = ( + params: IInheritanceEncryptMessageParams, +): Observable => + new Observable(observer => { + let finished = false; + let app: InheritanceApp | undefined; + + const cleanUp = async () => { + if (app) { + try { + await app.abort(); + } catch (error) { + logger.warn('Error in aborting inheritance encryptMessage'); + logger.warn(error); + } + } + }; + + const unsubscribe = () => { + if (!finished) { + finished = true; + cleanUp(); + } + }; + + const main = async () => { + try { + const { walletId, connection, cardLocation, personalMessage } = params; + + const events: Record = {} as any; + + const messages: Record = {}; + + if (personalMessage) { + messages[ENCRYPTED_DATA_SERIALIZATION_TAGS.NOMINEE_MESSAGE] = { + value: personalMessage, + verifyOnDevice: false, + }; + } + + if (cardLocation) { + messages[ENCRYPTED_DATA_SERIALIZATION_TAGS.WALLET_MESSAGE] = { + value: cardLocation, + verifyOnDevice: true, + }; + } + + app = await InheritanceApp.create(connection); + + const { encryptedPacket } = await app.encryptMessagesWithPin({ + walletId: hexToUint8Array(walletId), + messages, + onEvent: event => { + const deviceEvent = encryptMessageToDeviceEventMap[event]; + if (deviceEvent !== undefined) { + events[deviceEvent] = true; + } + + observer.next({ + type: 'Device', + device: { isDone: false, events }, + }); + }, + }); + + observer.next({ type: 'Device', device: { isDone: true, events } }); + observer.next({ + type: 'Result', + encryptedMessages: uint8ArrayToHex(encryptedPacket), + }); + + finished = true; + observer.complete(); + } catch (error) { + if (!finished) { + observer.error(error); + } + } + }; + + main(); + + return unsubscribe; + }); diff --git a/packages/app-support-inheritance/src/operations/encryptMessage/types.ts b/packages/app-support-inheritance/src/operations/encryptMessage/types.ts new file mode 100644 index 000000000..bcf8faa7d --- /dev/null +++ b/packages/app-support-inheritance/src/operations/encryptMessage/types.ts @@ -0,0 +1,27 @@ +import { IDeviceConnection } from '@cypherock/sdk-interfaces'; + +export interface IInheritanceEncryptMessageParams { + connection: IDeviceConnection; + walletId: string; + cardLocation?: string; + personalMessage?: string; +} + +export type IInheritanceEncryptMessageEventType = 'Result' | 'Device'; + +export enum InheritanceEncryptMessageDeviceEvent { + INIT = 0, + CONFIRMED = 1, + VERIFIED = 2, + PIN_CARD_TAPPED = 3, + MESSAGE_CARD_TAPPED = 4, +} + +export interface IInheritanceEncryptMessageEvent { + type: IInheritanceEncryptMessageEventType; + encryptedMessages?: string; + device?: { + isDone: boolean; + events: Record; + }; +} diff --git a/packages/app-support-inheritance/src/operations/index.ts b/packages/app-support-inheritance/src/operations/index.ts new file mode 100644 index 000000000..3e1af1de3 --- /dev/null +++ b/packages/app-support-inheritance/src/operations/index.ts @@ -0,0 +1,4 @@ +export * from './walletAuth'; +export * from './encryptMessage'; +export * from './decryptMessage'; +export * from './session'; diff --git a/packages/app-support-inheritance/src/operations/session/index.ts b/packages/app-support-inheritance/src/operations/session/index.ts new file mode 100644 index 000000000..01db0a557 --- /dev/null +++ b/packages/app-support-inheritance/src/operations/session/index.ts @@ -0,0 +1,2 @@ +export * from './start'; +export * from './stop'; diff --git a/packages/app-support-inheritance/src/operations/session/start.ts b/packages/app-support-inheritance/src/operations/session/start.ts new file mode 100644 index 000000000..2c5add295 --- /dev/null +++ b/packages/app-support-inheritance/src/operations/session/start.ts @@ -0,0 +1,61 @@ +import { InheritanceApp } from '@cypherock/sdk-app-inheritance'; +import { Observable } from 'rxjs'; + +import { + IInheritanceStartSessionEvent, + IInheritanceStartSessionParams, +} from './types'; + +import logger from '../../utils/logger'; + +export const startSession = ( + params: IInheritanceStartSessionParams, +): Observable => + new Observable(observer => { + let finished = false; + let app: InheritanceApp | undefined; + + const cleanUp = async () => { + if (app) { + try { + await app.abort(); + } catch (error) { + logger.warn('Error in aborting inheritance start session'); + logger.warn(error); + } + } + }; + + const unsubscribe = () => { + if (!finished) { + finished = true; + cleanUp(); + } + }; + + const main = async () => { + try { + const { connection } = params; + app = await InheritanceApp.create(connection); + + const { sessionId, sessionAge } = await app.startSession(); + observer.next({ type: 'Device', device: { isDone: true } }); + observer.next({ + type: 'Result', + sessionId, + sessionAge, + }); + + finished = true; + observer.complete(); + } catch (error) { + if (!finished) { + observer.error(error); + } + } + }; + + main(); + + return unsubscribe; + }); diff --git a/packages/app-support-inheritance/src/operations/session/stop.ts b/packages/app-support-inheritance/src/operations/session/stop.ts new file mode 100644 index 000000000..465c0d74f --- /dev/null +++ b/packages/app-support-inheritance/src/operations/session/stop.ts @@ -0,0 +1,59 @@ +import { InheritanceApp } from '@cypherock/sdk-app-inheritance'; +import { Observable } from 'rxjs'; + +import { + IInheritanceStopSessionEvent, + IInheritanceStopSessionParams, +} from './types'; + +import logger from '../../utils/logger'; + +export const stopSession = ( + params: IInheritanceStopSessionParams, +): Observable => + new Observable(observer => { + let finished = false; + let app: InheritanceApp | undefined; + + const cleanUp = async () => { + if (app) { + try { + await app.abort(); + } catch (error) { + logger.warn('Error in aborting inheritance stop session'); + logger.warn(error); + } + } + }; + + const unsubscribe = () => { + if (!finished) { + finished = true; + cleanUp(); + } + }; + + const main = async () => { + try { + const { connection } = params; + app = await InheritanceApp.create(connection); + + await app.closeSession(); + observer.next({ type: 'Device', device: { isDone: true } }); + observer.next({ + type: 'Result', + }); + + finished = true; + observer.complete(); + } catch (error) { + if (!finished) { + observer.error(error); + } + } + }; + + main(); + + return unsubscribe; + }); diff --git a/packages/app-support-inheritance/src/operations/session/types.ts b/packages/app-support-inheritance/src/operations/session/types.ts new file mode 100644 index 000000000..4d0b1ea60 --- /dev/null +++ b/packages/app-support-inheritance/src/operations/session/types.ts @@ -0,0 +1,29 @@ +import { IDeviceConnection } from '@cypherock/sdk-interfaces'; + +export interface IInheritanceStartSessionParams { + connection: IDeviceConnection; +} + +export type IInheritanceStartSessionEventType = 'Result' | 'Device'; + +export interface IInheritanceStartSessionEvent { + type: IInheritanceStartSessionEventType; + sessionId?: string; + sessionAge?: number; + device?: { + isDone: boolean; + }; +} + +export interface IInheritanceStopSessionParams { + connection: IDeviceConnection; +} + +export type IInheritanceStopSessionEventType = 'Result' | 'Device'; + +export interface IInheritanceStopSessionEvent { + type: IInheritanceStopSessionEventType; + device?: { + isDone: boolean; + }; +} diff --git a/packages/app-support-inheritance/src/operations/types.ts b/packages/app-support-inheritance/src/operations/types.ts new file mode 100644 index 000000000..cfe9a7fd4 --- /dev/null +++ b/packages/app-support-inheritance/src/operations/types.ts @@ -0,0 +1,5 @@ +export * from './walletAuth/types'; +export * from './encryptMessage/types'; +export * from './decryptMessage/types'; +export * from './session/types'; +export * from './common'; diff --git a/packages/app-support-inheritance/src/operations/walletAuth/index.ts b/packages/app-support-inheritance/src/operations/walletAuth/index.ts new file mode 100644 index 000000000..1696e90a7 --- /dev/null +++ b/packages/app-support-inheritance/src/operations/walletAuth/index.ts @@ -0,0 +1,111 @@ +import { + InheritanceApp, + AuthWalletEvent, + IAuthWalletSignatureAndKey, +} from '@cypherock/sdk-app-inheritance'; +import { hexToUint8Array, uint8ArrayToHex } from '@cypherock/sdk-utils'; +import { Observable } from 'rxjs'; + +import { + IInheritanceWalletAuthEvent, + IInheritanceWalletAuthParams, + InheritanceWalletAuthDeviceEvent, +} from './types'; + +import logger from '../../utils/logger'; + +const walletAuthToDeviceEventMap: Partial< + Record +> = { + [AuthWalletEvent.INIT]: InheritanceWalletAuthDeviceEvent.INIT, + [AuthWalletEvent.CONFIRMED]: InheritanceWalletAuthDeviceEvent.CONFIRMED, + [AuthWalletEvent.SEED_BASED_CARD_TAP]: + InheritanceWalletAuthDeviceEvent.SEED_BASED_CARD_TAPPED, + [AuthWalletEvent.CARD_PAIRING_CARD_TAP]: + InheritanceWalletAuthDeviceEvent.CARD_PAIRING_CARD_TAPPED, + [AuthWalletEvent.WALLET_BASED_CARD_TAP]: + InheritanceWalletAuthDeviceEvent.WALLET_BASED_CARD_TAPPED, +}; + +const parseSignatureAndKey = (params?: IAuthWalletSignatureAndKey) => { + if (!params) { + return undefined; + } + + return { + signature: uint8ArrayToHex(params.signature), + publicKey: params.publicKey ? uint8ArrayToHex(params.publicKey) : undefined, + }; +}; + +export const walletAuth = ( + params: IInheritanceWalletAuthParams, +): Observable => + new Observable(observer => { + let finished = false; + let app: InheritanceApp | undefined; + + const cleanUp = async () => { + if (app) { + try { + await app.abort(); + } catch (error) { + logger.warn('Error in aborting inheritance wallet auth'); + logger.warn(error); + } + } + }; + + const unsubscribe = () => { + if (!finished) { + finished = true; + cleanUp(); + } + }; + + const main = async () => { + try { + const { walletId, connection, isPublicKey, challenge, type } = params; + + const events: Record = {} as any; + + app = await InheritanceApp.create(connection); + + const { walletBased, seedBased } = await app.authWallet({ + walletId: hexToUint8Array(walletId), + challenge: hexToUint8Array(challenge), + type, + withPublicKey: isPublicKey, + onEvent: event => { + const deviceEvent = walletAuthToDeviceEventMap[event]; + if (deviceEvent !== undefined) { + events[deviceEvent] = true; + } + + observer.next({ + type: 'Device', + device: { isDone: false, events }, + }); + }, + }); + + observer.next({ type: 'Device', device: { isDone: true, events } }); + observer.next({ + type: 'Result', + walletBased: parseSignatureAndKey(walletBased), + seedBased: parseSignatureAndKey(seedBased), + }); + + finished = true; + observer.complete(); + } catch (error) { + if (!finished) { + observer.error(error); + } + } + }; + + main(); + + return unsubscribe; + }); diff --git a/packages/app-support-inheritance/src/operations/walletAuth/types.ts b/packages/app-support-inheritance/src/operations/walletAuth/types.ts new file mode 100644 index 000000000..138775b6c --- /dev/null +++ b/packages/app-support-inheritance/src/operations/walletAuth/types.ts @@ -0,0 +1,36 @@ +import { AuthWalletType } from '@cypherock/sdk-app-inheritance'; +import { IDeviceConnection } from '@cypherock/sdk-interfaces'; + +export interface IInheritanceWalletAuthParams { + connection: IDeviceConnection; + walletId: string; + challenge: string; + isPublicKey: boolean; + type: AuthWalletType; +} + +export type IInheritanceWalletAuthEventType = 'Result' | 'Device'; + +export enum InheritanceWalletAuthDeviceEvent { + INIT = 0, + CONFIRMED = 1, + SEED_BASED_CARD_TAPPED = 2, + CARD_PAIRING_CARD_TAPPED = 3, + WALLET_BASED_CARD_TAPPED = 4, +} + +export interface IInheritanceWalletAuthEvent { + type: IInheritanceWalletAuthEventType; + walletBased?: { + signature: string; + publicKey?: string; + }; + seedBased?: { + signature: string; + publicKey?: string; + }; + device?: { + isDone: boolean; + events: Record; + }; +} diff --git a/packages/app-support-inheritance/src/types.d.ts b/packages/app-support-inheritance/src/types.d.ts new file mode 100644 index 000000000..161a65b1e --- /dev/null +++ b/packages/app-support-inheritance/src/types.d.ts @@ -0,0 +1,7 @@ +import { ICysyncEnv } from '@cypherock/cysync-interfaces'; + +declare global { + interface Window { + cysyncEnv: ICysyncEnv; + } +} diff --git a/packages/app-support-inheritance/src/utils/logger.ts b/packages/app-support-inheritance/src/utils/logger.ts new file mode 100644 index 000000000..07e47841d --- /dev/null +++ b/packages/app-support-inheritance/src/utils/logger.ts @@ -0,0 +1,20 @@ +import { ILogger, LogCreator } from '@cypherock/cysync-interfaces'; +import { + createDefaultConsoleLogger, + updateLoggerObject, +} from '@cypherock/cysync-utils'; + +export const loggerServiceName = 'app-support-inheritance'; + +const logger: ILogger = { + ...createDefaultConsoleLogger(loggerServiceName), +}; + +export const updateLogger = (createLogger: LogCreator) => { + updateLoggerObject({ + currentLogger: logger, + newLogger: createLogger(loggerServiceName), + }); +}; + +export default logger; diff --git a/packages/app-support-inheritance/tests/01.create.ts b/packages/app-support-inheritance/tests/01.create.ts new file mode 100644 index 000000000..a5fe0f249 --- /dev/null +++ b/packages/app-support-inheritance/tests/01.create.ts @@ -0,0 +1,9 @@ +import { describe, expect, test } from '@jest/globals'; +import { InheritanceSupport } from '../src'; + +describe('01. Create', () => { + test('should be able to create Inheritance Support', async () => { + const support = new InheritanceSupport(); + expect(support).toBeDefined(); + }); +}); diff --git a/packages/app-support-inheritance/tsconfig.eslint.json b/packages/app-support-inheritance/tsconfig.eslint.json new file mode 100644 index 000000000..078e96315 --- /dev/null +++ b/packages/app-support-inheritance/tsconfig.eslint.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "outDir": "dist", + "lib": ["ES2015"] + }, + "extends": "./tsconfig.json", + "include": ["./src/**/*", "./tests/**/*"], + "exclude": ["node_modules", "src/coverage"] +} diff --git a/packages/app-support-inheritance/tsconfig.json b/packages/app-support-inheritance/tsconfig.json new file mode 100644 index 000000000..746daf4e1 --- /dev/null +++ b/packages/app-support-inheritance/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "outDir": "dist/esm", + "lib": ["DOM"] + }, + "extends": "@cypherock/tsconfig/browser.json", + "include": ["./src/**/*"], + "exclude": [ + "node_modules", + "src/coverage", + "src/**/__fixtures__/*.ts", + "src/**/__tests__/*.ts", + "src/**/__mocks__/*.ts" + ] +} diff --git a/packages/app-support-inheritance/tsconfig_cjs.json b/packages/app-support-inheritance/tsconfig_cjs.json new file mode 100644 index 000000000..40929696b --- /dev/null +++ b/packages/app-support-inheritance/tsconfig_cjs.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "dist/cjs", + "lib": ["DOM"], + "module": "CommonJS" + }, + "extends": "@cypherock/tsconfig/browser.json", + "include": ["./src/**/*"], + "exclude": [ + "node_modules", + "src/coverage", + "src/**/__fixtures__/*.ts", + "src/**/__tests__/*.ts", + "src/**/__mocks__/*.ts" + ] +} diff --git a/packages/core-services/src/db/version.ts b/packages/core-services/src/db/version.ts index a60dfddb9..8a5015df0 100644 --- a/packages/core-services/src/db/version.ts +++ b/packages/core-services/src/db/version.ts @@ -10,4 +10,5 @@ export const setDBVersions = (database: IDatabase) => { database.transactionNotificationClick.setVersion(0); database.transactionNotificationRead.setVersion(0); database.migration.setVersion(0); + database.inheritancePlan.setVersion(0); }; diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 87b5bb2ab..68915f000 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -4,4 +4,5 @@ export * from './account'; export * from './portfolio'; export * from './address'; export * from './transactions'; +export * from './inheritancePlan'; export { updateLogger } from './utils/logger'; diff --git a/packages/core-services/src/inheritancePlan.ts b/packages/core-services/src/inheritancePlan.ts new file mode 100644 index 000000000..3562c1b4e --- /dev/null +++ b/packages/core-services/src/inheritancePlan.ts @@ -0,0 +1,26 @@ +import { IDatabase, IInheritancePlan } from '@cypherock/db-interfaces'; + +export const insertInheritancePlan = async ( + db: IDatabase, + plan: Omit, +) => { + const existingPlan = await db.inheritancePlan.getOne({ + __id: plan.walletId, + }); + + if (existingPlan) { + await db.inheritancePlan.update( + { + __id: plan.walletId, + }, + { + ...plan, + }, + ); + } else { + await db.inheritancePlan.insert({ + __id: plan.walletId, + ...plan, + }); + } +}; diff --git a/packages/cysync-core-constants/src/constants.ts b/packages/cysync-core-constants/src/constants.ts index 24c382072..8314a5f98 100644 --- a/packages/cysync-core-constants/src/constants.ts +++ b/packages/cysync-core-constants/src/constants.ts @@ -7,4 +7,14 @@ export const constants = { termsOfUseLink: 'https://www.cypherock.com/terms', tutorialLink: 'https://youtube.com/playlist?list=PL0db5IfQ4iyo7GxBtFVBxxvanAngjdjLn&feature=shared', + inheritance: { + learnMore: 'https://www.cypherock.com', + termsOfServiceLink: 'https://www.cypherock.com/terms', + privacyPolicyLink: 'https://www.cypherock.com/terms', + silverPlanPurchaseTutorialLink: 'https://youtu.be/bRmNxrOf3VM', + silverPlanPurchaseLink: 'https://www.cypherock.com', + goldPlanPurchaseTutorialLink: 'https://youtu.be/bRmNxrOf3VM', + goldPlanPurchaseLink: 'https://www.cypherock.com', + maxUserDetailsInputLength: 50, + }, }; diff --git a/packages/cysync-core-constants/src/i18n/lang/ar-AE.json b/packages/cysync-core-constants/src/i18n/lang/ar-AE.json index da227aa51..2e8331ade 100644 --- a/packages/cysync-core-constants/src/i18n/lang/ar-AE.json +++ b/packages/cysync-core-constants/src/i18n/lang/ar-AE.json @@ -3,6 +3,10 @@ "help": "مساعدة", "back": "عودة", "allWallets": "جميع Wallets", + "labels": { + "email": "Email", + "required": "Required" + }, "buttons": { "addWallet": "إضافة Wallets", "addAccount": "إضافة حساب", @@ -40,7 +44,37 @@ "showQRCode": "إظهار رمز الاستجابة السريعة", "editAccount": "تحرير الحساب", "submit": "إرسال", - "showMore": "عرض المزيد" + "showMore": "عرض المزيد", + "resendOTP": "Resend OTP in ${timeout}s", + "next": "Next", + "setup": "Setup", + "sync": "Sync", + "learnMore": "Learn More", + "checkout": "Checkout", + "yes": "Yes", + "no": "No", + "saveAndContinue": "Save and continue", + "exitWithoutSaving": "Exit without saving", + "saveChanges": "Save changes", + "edit": "Edit", + "tryAgain": "Try Again" + }, + "dashboard": { + "wallet": { + "renewNow": "Renew Now", + "buyNow": "Buy Now", + "created": "Created", + "expiredOn": "Expired on", + "expiresIn": "Expires in", + "expiry": "Expiry", + "expiring": "Expiring", + "expired": "Expired", + "pending": "Pending", + "silver": "Silver", + "gold": "Gold", + "hours": "Hours", + "setupCover": "Setup Cypherock Cover" + } }, "tooltips": { "downloadCsv": "تصدير كملف CSV" @@ -616,9 +650,11 @@ "receiveCrypto": "استقبال العملات المشفرة", "history": "السجل", "walletConnect": "Wallet Connect", + "cypherockCover": "Cypherock Cover", "tutorial": "تعليمات", "settings": "الإعدادات", "help": "مساعدة", + "new": "New", "tooltip": { "walletDeleted": "${walletName} تم حذفه من Cypherock X1 Vault" } @@ -912,6 +948,9 @@ "heading": "جهاز X1 Vault الخاص بك قد انتهى الوقت المحدد له", "subtext": "انتقل إلى القائمة الرئيسية على الجهاز وحاول مرة أخرى" }, + "APP_0207": { + "heading": "Could not establish session on device" + }, "APP_0400": { "heading": "خطأ غير معروف في X1 Card", "subtext": "أعد المحاولة أو انقر فوق المساعدة للعثور على حل" @@ -1023,12 +1062,113 @@ "serverErrors": { "SER_0000": { "heading": "يواجه cySync app الخاص بك بعض مشكلات الاتصال", - "subtext": "أعد توصيل الجهاز وحاول مرة أخرى وإذا استمرت المشكلة ، اتصل بدعم Cypherock للحصول على المساعدة" + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" }, "SER_0001": { "heading": "يرجى الاتصال بالإنترنت للمتابعة" + }, + "SER_1001": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1003": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1004": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1005": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1006": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1007": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1008": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1009": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1010": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1011": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1012": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1013": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1014": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1015": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1016": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1017": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1018": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1019": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1020": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1021": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1022": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1023": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1024": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1025": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1026": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" } }, + "videoPlaybackError": "No Internet connection. Please check your connection and ", "serverCoinErrors": { "default": { "DEF_0000": { @@ -1041,7 +1181,8 @@ }, "validation": { "generic": { - "required": "هذا الحقل مطلوب" + "required": "هذا الحقل مطلوب", + "englishOnly": "Only ASCII characters and English text are allowed" }, "email": { "invalid": "هذا ليس بريدًا إلكترونيًا صالحًا" @@ -1472,6 +1613,338 @@ "info": "هذا هو ${derivationSchemeName} xpub. عند استيراده إلى محفظة تابعة لجهة خارجية، اختر نفس وضع الاستنتاج", "advanced": "متقدم" } + }, + "inheritanceSyncPlans": { + "enterEmail": { + "title": "Please enter your primary email associated with this plan", + "subTitle": "An OTP will be sent to this email", + "button": "Verify" + }, + "verifyEmail": { + "title": "OTP verification" + } + }, + "inheritancePlanLogin": { + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + } + }, + "fetchData": { + "title": "Please wait while we fetch the plan data", + "subTitle": "Do not disconnect or close the app" + }, + "verifyEmail": { + "title": "OTP Verification" + } + }, + "inheritanceEditExecutorMessage": { + "fetchData": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "editMessage": { + "title": "Executor Message", + "form": { + "messageField": { + "label": "Message", + "placeholder": "Executor Message" + } + }, + "buttons": { + "exit": "Exit without saving", + "save": "Save changes" + }, + "loading": { + "title": "Please wait while we update the data", + "subtitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Executor message successfully encrypted" + } + }, + "inheritanceEditReminderTime": { + "fetchData": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "reminderSetup": { + "title": "Reminder setup", + "reminderInfo": { + "subtitle": "What is reminder time?", + "subtext": "Reminder Time is the interval at which you'll receive a confirmation email from Cypherock to verify your continued well-being. If no response is received within two months of the initial email, the inheritance process will commence and your nominee will receive the recovery instruction email from Cypherock" + }, + "form": { + "reminderField": { + "label": "Set reminder time interval", + "month": "Month", + "months": "Months" + } + }, + "currentReminder": "The current reminder is set at ${month}", + "loading": { + "title": "Please wait while we update the data", + "subtitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Reminders duration updated successfully" + } + }, + "inheritanceEditUserDetails": { + "editDetails": { + "title": "Edit ${userType} Details", + "userTypes": { + "owner": "Owner", + "nominee": "Nominee", + "executor": "Executor" + }, + "buttons": { + "verifyEmail": "Verify Email" + } + }, + "verifyOtp": { + "loading": { + "title": "Please wait while we update the data", + "subtext": "Do not disconnect or close the app" + } + }, + "success": { + "title": "${userType} details successfully updated" + } + }, + "inheritancePinRecovery": { + "title": "PIN Recovery", + "sync": { + "name": "Syncing", + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirmAuth": "Confirm wallet authentication on device", + "tapCard": "Tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 2 beep sounds" + } + }, + "verifyOtp": { + "title": "OTP verification" + }, + "fetch": { + "title": "Please wait while we fetch the encrypted PIN", + "subTitle": "Do no disconnect or close the app" + } + }, + "decryptPin": { + "name": "Decryption", + "title": "Follow instructions on the X1 Vault", + "actions": { + "confirmOnDevice": "Confirm decryption on device", + "tapCard": "Tap any one card" + }, + "messageBox": { + "warning": "The PIN will be decrypted by the card" + }, + "error": { + "title": "PIN Decryption Failed", + "subTitle": "PIN decryption was unsuccessful. Wallet ${wallet} was not found in the card" + } + }, + "viewPin": { + "name": "View PIN", + "title": "Follow instructions on the X1 Vault", + "subTitle": "PIN for wallet ", + "actions": { + "viewDevice": "View the PIN on your device and confirm" + } + }, + "success": { + "name": "Confirmation", + "title": "Congratulations, Pin recovery is complete" + } + }, + "inheritanceEditEncryptedMessage": { + "confirmation": { + "title": "Are you sure you want to edit the encrypted nominee message?", + "subTitle": "You will need the device and at least one card to encrypt the message again after editing" + }, + "syncing": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "decryption": { + "wallet": { + "title": "Follow instructions on the X1 Vault", + "actions": { + "confirm": "Confirm decryption on device", + "tapCard": "Tap any card" + }, + "messageBox": { + "warning": "The message will be decrypted by the X1 card" + }, + "decryption": { + "title": "Please wait while we decrypt the data", + "subTitle": "Do not disconnect or close the app" + } + } + }, + "editMessage": { + "title": "Nominee message", + "subTitle": "The nominee messages will be end-to-end encrypted", + "form": { + "cardLocationField": { + "label": "Card location & Passphrase", + "placeholder": "Write the location of the card", + "tooltip": "Write down the location of another card for the nominee to fetch. Nominee will only be able to recover crypto assets after getting another card. Remember this card should be different than the card(s) you may have given to your nominee(s).\nYou can also include the passphrase in this message" + }, + "personalMessageField": { + "label": "Personal message", + "placeholder": "Write your message", + "tooltip": "This can include your will/message for the nominee and other important pieces of information that you may want your nominee to know such as your crypto exchange account details, coins you have amongst others." + } + }, + "messageBox": { + "warning": "Make sure you describe the card's location clearly for the nominee" + } + }, + "confirmMessage": { + "title": "Follow instructions on the X1 Vault to verify the nominee message", + "subTitle": "You can encrypt the message in the next step", + "actions": { + "confirmOnDevice": "Confirm on device", + "verifyLocation": "Verify Card location in your device" + }, + "messageBox": { + "danger": "Read through the whole text and verify that the message shown in the device is same as you entered in cySync" + } + }, + "encryption": { + "title": "Follow instructions on the X1 Vault to encrypt the nominee message", + "subTitle": "Lift your card after 2 beep sounds", + "tooltip": "", + "actions": { + "enterPinAndTap": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The nominee message will be encrypted by the Card" + }, + "syncing": { + "title": "Please wait while we save the encrypted data on the server", + "subTitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Nominee message successfully updated" + } + }, + "inheritanceEstateRecovery": { + "title": "Estate recovery", + "instructions": { + "name": "Clear Device Data", + "dialogs": { + "settings": { + "title": "From the Main Menu of your X1 Vault, click \"Settings\"" + }, + "clearData": { + "title": "Click \"Clear Device Data\" on your X1 Vault" + }, + "confirmClearData": { + "title": "Confirm \"Clear device data\" on the X1 Vault" + }, + "tapCards": { + "title": "Tap any 2 old X1 Cards below the X1 Vault", + "subTitle": "Do not lift until you hear a beep sound", + "messageBoxList": [ + { + "warning": "Tap the cards you used during the initial Cypherock setup process, not the cards given by the wallet owner" + }, + { + "warning": "Your device will restart after this process" + } + ] + } + } + }, + "wallet": { + "name": "Wallet", + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Tap the card you received as a nominee" + }, + "messageBox": { + "warning": "Do not lift until you hear 2 beep sound" + }, + "verification": { + "title": "OTP verification" + }, + "syncing": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + } + }, + "decryption": { + "name": "Decryption", + "device": { + "title": "Follow instructions on the X1 Vault", + "subTitle": "Do not lift the card till you hear a beep sound", + "actions": { + "confirm": "Confirm decryption on device", + "tapCard": "Tap the card you received as a nominee" + }, + "messageBox": { + "warning": "The message will be decrypted by the card you received as a nominee" + } + }, + "error": { + "title": "Decrypted message", + "message": "Update was unsuccessful. Wallet ABC was not found in the card" + } + }, + "viewPin": { + "name": "View PIN", + "title": "Follow instructions on the X1 Vault", + "subTitle": "PIN for wallet", + "actions": { + "view": "View & copy the PIN displayed on your device and then confirm" + }, + "messageBox": { + "warning": "You will need this PIN to access crypto assets on this wallet. Make sure you do not forget it." + } + }, + "viewMessage": { + "name": "View Message", + "title": "Message from ${name}", + "tooltip": "These messages were written by the wallet owner for you. They contain important information about how to recover their crypto assets stored on the Cypherock X1. \nNote- Make sure you keep these messages safe and private. ", + "form": { + "cardLocationField": { + "label": "Card location", + "placeholder": "Write the location of the card", + "tooltip": "This is the location of another card written by wallet owner for you to fetch. You will only be able to recover the crypto assets after getting another card" + }, + "personalMessageField": { + "label": "Personal message", + "placeholder": "Write your message", + "tooltip": "This includes a personal message written by wallet owner for you" + }, + "checkBox": { + "label": "I have read and copied the above messages" + } + } + }, + "confirmation": { + "name": "Confirmation", + "success": { + "title": "Congratulations, you just decrypted the message from ", + "subTitle": "Watch the video to understand the next steps" + } + } } }, "toggle": { @@ -1574,5 +2047,555 @@ } } } + }, + "otp": { + "title": "Enter OTP", + "wrongOtpTitle": "Wrong OTP", + "successRedirectTitle": "Please wait while we redirect you...", + "triesRemaining": "${retries} tries remaining", + "infoTexts": [ + "An email has been sent to **${email}**", + "Please enter the code received on the email" + ], + "noRetries": { + "title": "You have 0 tries remaining", + "subTitle": "Please exit the flow and restart" + }, + "buttons": { + "resendWithTimeout": "Resend OTP in ${timeout}s" + } + }, + "inheritance": { + "title": "Cypherock Cover", + "plans": { + "silver": { + "title": "Silver", + "description": "Secure your wallet PIN with us and easily recover it if forgotten, ensuring continuous access to your crypto assets" + }, + "gold": { + "title": "Gold", + "description": "Ensure your crypto assets are inherited by loved ones effortlessly, bypassing complex legal restrictions for smooth transfers" + } + }, + "choosePlan": { + "title": "Choose a plan", + "plans": { + "buttonText": "SELECT", + "popularTagText": "MOST POPULAR", + "features": [ + "No KYC required", + "Multi-Chain", + "PIN Recovery", + "Estate Recovery", + "Passphrase Recovery" + ] + } + }, + "homePage": { + "headers": { + "owner": { + "title": "Owner", + "subtitle": "Setup recovery for your wallets" + }, + "nominee": { + "title": "Nominee", + "subtitle": "You are a nominee of the following wallets" + } + }, + "setup": { + "setupCover": { + "title": "Setup Cypherock Cover", + "subTitle": "Protect your crypto assets and ensure smooth transfer to your beneficiaries in unforeseen circumstances" + }, + "syncFromMail": { + "title": "Sync from Email", + "subTitle": "If you're already part of a Cypherock Cover plan, either as the plan owner or a beneficiary" + }, + "learnMore": { + "title": "What is Cypherock Cover?", + "subTitle": "Learn how Cypherock Cover secures your crypto assets and ensures their seamless transfer to your loved ones" + } + } + }, + "planDetails": { + "walletDetails": { + "createdOn": "Created on", + "expiringOn": "Expiring on", + "reminderPeriodField": { + "label": "Reminder Period", + "input": "Every ${month} month", + "inputPlural": "Every ${month} months" + } + }, + "ownerDetails": { + "title": "Owner Details", + "form": { + "userNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + } + } + }, + "nomineeDetails": { + "title": "Nominee #${number}", + "form": { + "nomineeNameField": { + "label": "Nominee Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "encryptedMessage": { + "label": "Encrypted Message" + } + } + }, + "executorDetails": { + "title": "Executor", + "form": { + "nomineeNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "executorMessage": { + "label": "Executor Message" + }, + "assignTo": { + "label": "Assigned to" + } + } + } + }, + "buttons": { + "syncFromEmail": "Sync from Email", + "recoverPin": "Recover PIN", + "renewPlan": "Renew Plan", + "unlock": "Unlock", + "upgradePlan": "Upgrade Plan" + }, + "termsOfService": { + "title": "Terms of Use", + "termsOfService": "Terms of Service", + "privacyPolicy": "Privacy Policy", + "checkBoxLabel": "I have read and agree to Terms of service and Privacy policy" + }, + "dialog": { + "userDetails": { + "form": { + "name": "Name", + "emailField": { + "label": "Primary Email", + "tooltip": "Enter the email on which you want to receive all reminder emails" + }, + "alternateEmail": "Alternate Email" + }, + "error": { + "sameEmail": "Alternate email cannot be same as primary email" + } + }, + "verifyOTP": { + "primaryEmailOTP": { + "title": "OTP Verification for primary email" + }, + "alternateEmailOTP": { + "title": "OTP Verification for alternate email" + } + }, + "payment": { + "heading": "Buy the plan on the Cypherock website", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase", + "form": { + "promoField": { + "label": "Promo code / coupon", + "placeholder": "Enter code" + } + }, + "couponInput": { + "appliedButtonText": "Coupon applied:", + "applyButtonText": "Apply" + }, + "noOfYear": "Number of years", + "total": "Total", + "year": "year", + "error": { + "errorHeading": "Invalid Coupon Code", + "subtext": "Please try a different coupon code" + } + } + }, + "banner": { + "title": "Secure your crypto legacy with Cypherock Cover - Ensure your assets reach your loved ones", + "buttons": { + "knowMore": "Know More" + } + } + }, + "inheritanceSilverPlanPurchase": { + "title": "Silver Plan Setup", + "instructions": { + "heading": "Instructions", + "ensure": { + "title": "Ensure the following before you continue", + "instructions": [ + "Make sure you have 15-30 minutes to set it up", + "You are present in a safe environment", + "You need to have one device and one card handy to set it up" + ] + }, + "video": { + "title": "Watch the tutorial video before you proceed further" + } + }, + "wallet": { + "heading": "Wallet", + "selectWallet": { + "title": "Select the wallet you want to cover in this plan", + "subTitle": "For setting up additional wallets individual plans can be created later", + "tooltip": "This wallet is not having a PIN setup, create a new wallet or select a different wallet having PIN setup" + }, + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 3 beep sounds" + } + } + }, + "email": { + "heading": "Owner", + "userDetails": { + "title": "Your Details", + "subTitle": "All the fields are mandatory", + "buttons": { + "sendOTP": "Send OTP" + } + } + }, + "encryption": { + "heading": "Encryption", + "device": { + "title": "Follow instructions on the X1 Vault to encrypt the PIN", + "subTitle": "Lift your card after 2 beep sounds", + "tooltip": "Tap any one X1 card below the X1 device to encrypt the message and do not remove the card until you hear 2 beep sounds", + "actions": { + "confirm": "Confirm encryption on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The PIN will be encrypted by the X1 card" + } + }, + "loading": { + "title": "Please wait while we save the encrypted data on the server", + "subTitle": "Do not disconnect or close the app" + }, + "success": { + "title": "PIN is successfully encrypted and saved", + "subTitle": "To complete your silver plan setup, you can now proceed with the plan purchase" + } + }, + "checkout": { + "heading": "Checkout", + "payment": { + "title": "Complete your payment", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase" + }, + "success": { + "title": "Thank you!", + "subtext": "Your Silver Plan is now activated" + } + } + }, + "inheritanceGoldPlanPurchase": { + "title": "Gold Plan Setup", + "instructions": { + "heading": "Instructions", + "ensure": { + "title": "Ensure the following before you continue", + "instructions": [ + "Make sure you have 15-30 minutes to set it up", + "You are present in a safe environment", + "You need to have one device and one card handy to set it up" + ] + }, + "video": { + "title": "Watch the tutorial video before you proceed", + "subTitle": "The video will help you take the right decisions" + } + }, + "wallet": { + "heading": "Wallet", + "selectWallet": { + "title": "Select the wallet you want to cover in this plan", + "subTitle": "For setting up additional wallets, individual plans can be created later", + "tooltip": "This wallet is not having a PIN setup, create a new wallet or select a different wallet having PIN setup" + }, + "walletAuth": { + "heading": "Wallet Authentication", + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirmOnDevice": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 3 beep sounds" + }, + "error": { + "title": "Wallet Authentication failed", + "subtext": "We could not authenticate the selected wallet", + "messageBox": { + "warning": "This usually happens when the wallets in the Cysync and the device are not in sync. Sync the wallets between the Cysync and the device or choose a different wallet" + } + } + } + }, + "email": { + "heading": "Owner", + "userDetails": { + "title": "Your Details", + "form": { + "name": "Name", + "emailField": { + "label": "Primary Email", + "tooltip": "Enter the email on which you want to receive all reminder emails" + }, + "alternateEmail": "Alternate Email" + }, + "buttons": { + "sendOTP": "Send OTP" + } + }, + "primaryEmailOTP": { + "title": "OTP Verification for primary email" + }, + "alternateEmailOTP": { + "title": "OTP Verification for alternate email" + } + }, + "nomineeAndExecutor": { + "heading": "Nominees & Executor", + "select": { + "title": "How many nominees do you want?", + "subtitle": "You can have up to 2 nominees for this will", + "tooltip": "In case if you select two nominees then the 2nd nominee will receive the recovery email three months after the 1st nominee receives it\nNote - Both nominees will receive the same recovery email and same message written by you on the CySync app", + "options": { + "descOne": "Select if you want to have only one nominee", + "descTwo": "Select if you want to have two nominees" + } + }, + "nomineeDetails": { + "first": { + "title": "Add first nominee", + "warning": "Make sure you give a card to your nominee", + "tooltip": "Your Nominee will receive recovery instructions on this email" + }, + "second": { + "title": "Add second nominee", + "warning": "Make sure you give a card to your second nominee" + }, + "confirm": { + "title": "Do you want to verify nominee emails?", + "subtext": "An OTP will be sent to the provided emails. If you do not want to inform the nominee about this plan, you can skip verification of emails.", + "warning": "If you do not verify the nominee email then Cypherock emails may land in spam folder" + } + }, + "executor": { + "select": { + "title": "Do you want to have an executor for this will?", + "subtext": "The executor will help the nominee to recover funds ", + "tooltip": "An executor/helper can be someone you trust who does not have any financial stake but can be assigned as a helper to the nominee for funds recovery\nThe executor will receive the email on the same time as the nominee.", + "options": { + "one": { + "yes": "Yes", + "desc": "A tech savvy person whom you trust" + }, + "two": { + "no": "No", + "desc": "If your nominees are tech savvy" + } + } + }, + "executorDetails": { + "title": "Add executor", + "tooltip": "Executor will receive instructions to help nominee on this email", + "radio": { + "label": "Choose a nominee to link with executor", + "tooltip": "Select the nominee you want to link with the executor. If you choose Nominee 1 then the executor will receive their email written by you on same time as Nominee 1.\n\nIf you choose Nominee 2 then the executor will receive their email written by you on same time as Nominee 2 i.e, 3 months after Nominee 1.", + "options": { + "labelPrefix": "Nominee " + } + } + } + } + }, + "message": { + "heading": "Message", + "tutorial": { + "title": "Watch the tutorial video before you proceed further", + "subtext": "How to write the final messages" + }, + "nominee": { + "title": "Draft the final message for the nominee", + "subtitle": "The nominee messages will be end-to-end encrypted", + "tooltip": "End-to-end encryption keeps your messages between you and the nominee you choose. Not even Cypherock can read them", + "form": { + "locationField": { + "label": "Card Location & Passphrase", + "placeholder": "Write the location of the card", + "tooltip": "Write down the location of another card for the nominee to fetch. Nominee will only be able to recover crypto assets after getting another card. Remember this card should be different than the card(s) you may have given to your nominee(s).\nYou can also include the passphrase in this message." + }, + "personalMessage": { + "label": "Personal Message", + "placeholder": "Write your message", + "tooltip": "This can include your will/message for the nominee and other important pieces of information that you may want your nominee to know such as your crypto exchange account details, coins you have amongst others." + } + }, + "messageBox": { + "warning": "Make sure you describe the card's location clearly for the nominee" + } + }, + "executor": { + "title": "Add a message for the executor", + "subtitle": "Please note that the message to the executor is not encrypted", + "tooltip": "This message will be received by the executor in case recovery is triggered, it's recommended to NOT add any critical information here", + "form": { + "messageField": { + "label": "Message", + "placeholder": "Write your message" + } + } + } + }, + "reminder": { + "heading": "Reminder", + "title": "Reminder setup" + }, + "summary": { + "heading": "Summary", + "title": "Summary", + "subtitle": "Please verify all the details before it gets encrypted with the device", + "ownerDetails": { + "title": "Owner Details", + "form": { + "userNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "reminderPeriodField": { + "label": "Reminder Period", + "input": "Every ${month} month", + "inputPlural": "Every ${month} months" + } + } + }, + "nomineeDetails": { + "title": "Nominee #", + "form": { + "nomineeNameField": { + "label": "Nominee Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + } + } + }, + "cardLocation": { + "title": "Card Location & Passphrase" + }, + "personalMessage": { + "title": "Personal Message" + }, + "executorDetails": { + "title": "Executor", + "form": { + "nomineeNameField": { + "label": "Nominee Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "assignTo": { + "label": "Assigned to" + } + } + }, + "executorMessage": { + "title": "Executor Message" + } + }, + "encryption": { + "heading": "Encryption", + "confirm": { + "title": "Follow instructions on the X1 Vault to verify the nominee message", + "subtext": "You can encrypt the message in the next step", + "actions": { + "confirmOnDevice": "Confirm message verification on device", + "verifyLocation": "Verify Card Location in your device" + }, + "messageBox": { + "danger": "Read through the whole text and verify that the message shown in the device is same as you entered in cySync" + } + }, + "device": { + "title": "Follow instructions on the X1 Vault to encrypt the wallet PIN", + "subtext": "Lift your card after 2 beep sounds ", + "tooltip": "Tap any one X1 card below the X1 device to encrypt the message and do not remove the card until you hear 2 beep sounds", + "actions": { + "confirmOnDevice": "Confirm encryption on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The PIN will be encrypted by the X1 card" + } + }, + "loading": { + "title": "Please wait while we send the encrypted data to the server", + "subTitle": "Do not disconnect or close the app" + }, + "success": { + "title": "Nominee message successfully encrypted", + "subtext": "To complete your gold plan setup, you can now proceed with the plan purchase" + } + }, + "checkout": { + "heading": "Checkout", + "payment": { + "title": "Complete your payment", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase" + }, + "greeting": { + "title": "Thank you!", + "subtext": "Your Gold Plan is now activated" + } + } } } diff --git a/packages/cysync-core-constants/src/i18n/lang/de-DE.json b/packages/cysync-core-constants/src/i18n/lang/de-DE.json index c8f817299..9da944616 100644 --- a/packages/cysync-core-constants/src/i18n/lang/de-DE.json +++ b/packages/cysync-core-constants/src/i18n/lang/de-DE.json @@ -3,6 +3,10 @@ "help": "Help", "back": "Zurück", "allWallets": "Alle Wallets", + "labels": { + "email": "Email", + "required": "Required" + }, "buttons": { "addWallet": "Wallet hinzufügen", "addAccount": "Konto hinzufügen", @@ -40,7 +44,37 @@ "showQRCode": "QR-Code anzeigen", "editAccount": "Konto bearbeiten", "submit": "Absenden", - "showMore": "Mehr anzeigen" + "showMore": "Mehr anzeigen", + "resendOTP": "Resend OTP in ${timeout}s", + "next": "Next", + "setup": "Setup", + "sync": "Sync", + "learnMore": "Learn More", + "checkout": "Checkout", + "yes": "Yes", + "no": "No", + "saveAndContinue": "Save and continue", + "exitWithoutSaving": "Exit without saving", + "saveChanges": "Save changes", + "edit": "Edit", + "tryAgain": "Try Again" + }, + "dashboard": { + "wallet": { + "renewNow": "Renew Now", + "buyNow": "Buy Now", + "created": "Created", + "expiredOn": "Expired on", + "expiresIn": "Expires in", + "expiry": "Expiry", + "expiring": "Expiring", + "expired": "Expired", + "pending": "Pending", + "silver": "Silver", + "gold": "Gold", + "hours": "Hours", + "setupCover": "Setup Cypherock Cover" + } }, "tooltips": { "downloadCsv": "Als CSV exportieren" @@ -618,9 +652,11 @@ "receiveCrypto": "Krypto empfangen", "history": "Verlauf", "walletConnect": "Wallet Connect", + "cypherockCover": "Cypherock Cover", "tutorial": "Anleitung", "settings": "Einstellungen", "help": "Help", + "new": "New", "tooltip": { "walletDeleted": "${walletName} wurde aus deinem Cypherock X1 Vault gelöscht" } @@ -917,6 +953,9 @@ "heading": "Ihr X1 Vault hat eine Zeitüberschreitung", "subtext": "Navigieren Sie zum Hauptmenü auf dem Gerät und versuchen Sie es erneut" }, + "APP_0207": { + "heading": "Could not establish session on device" + }, "APP_0400": { "heading": "Unbekannter X1 Card-Fehler", "subtext": "Erneut versuchen oder auf Hilfe klicken, um eine Lösung zu finden" @@ -1028,12 +1067,113 @@ "serverErrors": { "SER_0000": { "heading": "Ihre CySync App hat ein Verbindungsproblem", - "subtext": "Verbinden Sie das Gerät erneut und versuchen Sie es erneut und wenn das Problem weiterhin besteht, kontaktieren Sie den Cypherock Support für Unterstützung" + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" }, "SER_0001": { "heading": "Bitte verbinden Sie sich mit dem Internet, um fortzufahren" + }, + "SER_1001": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1003": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1004": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1005": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1006": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1007": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1008": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1009": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1010": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1011": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1012": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1013": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1014": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1015": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1016": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1017": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1018": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1019": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1020": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1021": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1022": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1023": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1024": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1025": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1026": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" } }, + "videoPlaybackError": "No Internet connection. Please check your connection and ", "serverCoinErrors": { "default": { "DEF_0000": { @@ -1046,7 +1186,8 @@ }, "validation": { "generic": { - "required": "Dieses Feld ist erforderlich" + "required": "Dieses Feld ist erforderlich", + "englishOnly": "Only ASCII characters and English text are allowed" }, "email": { "invalid": "Dies ist keine gültige E-Mail-Adresse" @@ -1477,6 +1618,338 @@ "info": "Dies ist ein ${derivationSchemeName} xpub. Wenn Sie es in eine Drittanbieter-Wallet importieren, wählen Sie denselben Derivationsmodus", "advanced": "Erweitert" } + }, + "inheritanceSyncPlans": { + "enterEmail": { + "title": "Please enter your primary email associated with this plan", + "subTitle": "An OTP will be sent to this email", + "button": "Verify" + }, + "verifyEmail": { + "title": "OTP verification" + } + }, + "inheritancePlanLogin": { + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + } + }, + "fetchData": { + "title": "Please wait while we fetch the plan data", + "subTitle": "Do not disconnect or close the app" + }, + "verifyEmail": { + "title": "OTP Verification" + } + }, + "inheritanceEditExecutorMessage": { + "fetchData": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "editMessage": { + "title": "Executor Message", + "form": { + "messageField": { + "label": "Message", + "placeholder": "Executor Message" + } + }, + "buttons": { + "exit": "Exit without saving", + "save": "Save changes" + }, + "loading": { + "title": "Please wait while we update the data", + "subtitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Executor message successfully encrypted" + } + }, + "inheritanceEditReminderTime": { + "fetchData": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "reminderSetup": { + "title": "Reminder setup", + "reminderInfo": { + "subtitle": "What is reminder time?", + "subtext": "Reminder Time is the interval at which you'll receive a confirmation email from Cypherock to verify your continued well-being. If no response is received within two months of the initial email, the inheritance process will commence and your nominee will receive the recovery instruction email from Cypherock" + }, + "form": { + "reminderField": { + "label": "Set reminder time interval", + "month": "Month", + "months": "Months" + } + }, + "currentReminder": "The current reminder is set at ${month}", + "loading": { + "title": "Please wait while we update the data", + "subtitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Reminders duration updated successfully" + } + }, + "inheritanceEditUserDetails": { + "editDetails": { + "title": "Edit ${userType} Details", + "userTypes": { + "owner": "Owner", + "nominee": "Nominee", + "executor": "Executor" + }, + "buttons": { + "verifyEmail": "Verify Email" + } + }, + "verifyOtp": { + "loading": { + "title": "Please wait while we update the data", + "subtext": "Do not disconnect or close the app" + } + }, + "success": { + "title": "${userType} details successfully updated" + } + }, + "inheritancePinRecovery": { + "title": "PIN Recovery", + "sync": { + "name": "Syncing", + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirmAuth": "Confirm wallet authentication on device", + "tapCard": "Tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 2 beep sounds" + } + }, + "verifyOtp": { + "title": "OTP verification" + }, + "fetch": { + "title": "Please wait while we fetch the encrypted PIN", + "subTitle": "Do no disconnect or close the app" + } + }, + "decryptPin": { + "name": "Decryption", + "title": "Follow instructions on the X1 Vault", + "actions": { + "confirmOnDevice": "Confirm decryption on device", + "tapCard": "Tap any one card" + }, + "messageBox": { + "warning": "The PIN will be decrypted by the card" + }, + "error": { + "title": "PIN Decryption Failed", + "subTitle": "PIN decryption was unsuccessful. Wallet ${wallet} was not found in the card" + } + }, + "viewPin": { + "name": "View PIN", + "title": "Follow instructions on the X1 Vault", + "subTitle": "PIN for wallet ", + "actions": { + "viewDevice": "View the PIN on your device and confirm" + } + }, + "success": { + "name": "Confirmation", + "title": "Congratulations, Pin recovery is complete" + } + }, + "inheritanceEditEncryptedMessage": { + "confirmation": { + "title": "Are you sure you want to edit the encrypted nominee message?", + "subTitle": "You will need the device and at least one card to encrypt the message again after editing" + }, + "syncing": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "decryption": { + "wallet": { + "title": "Follow instructions on the X1 Vault", + "actions": { + "confirm": "Confirm decryption on device", + "tapCard": "Tap any card" + }, + "messageBox": { + "warning": "The message will be decrypted by the X1 card" + }, + "decryption": { + "title": "Please wait while we decrypt the data", + "subTitle": "Do not disconnect or close the app" + } + } + }, + "editMessage": { + "title": "Nominee message", + "subTitle": "The nominee messages will be end-to-end encrypted", + "form": { + "cardLocationField": { + "label": "Card location & Passphrase", + "placeholder": "Write the location of the card", + "tooltip": "Write down the location of another card for the nominee to fetch. Nominee will only be able to recover crypto assets after getting another card. Remember this card should be different than the card(s) you may have given to your nominee(s).\nYou can also include the passphrase in this message" + }, + "personalMessageField": { + "label": "Personal message", + "placeholder": "Write your message", + "tooltip": "This can include your will/message for the nominee and other important pieces of information that you may want your nominee to know such as your crypto exchange account details, coins you have amongst others." + } + }, + "messageBox": { + "warning": "Make sure you describe the card's location clearly for the nominee" + } + }, + "confirmMessage": { + "title": "Follow instructions on the X1 Vault to verify the nominee message", + "subTitle": "You can encrypt the message in the next step", + "actions": { + "confirmOnDevice": "Confirm on device", + "verifyLocation": "Verify Card location in your device" + }, + "messageBox": { + "danger": "Read through the whole text and verify that the message shown in the device is same as you entered in cySync" + } + }, + "encryption": { + "title": "Follow instructions on the X1 Vault to encrypt the nominee message", + "subTitle": "Lift your card after 2 beep sounds", + "tooltip": "", + "actions": { + "enterPinAndTap": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The nominee message will be encrypted by the Card" + }, + "syncing": { + "title": "Please wait while we save the encrypted data on the server", + "subTitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Nominee message successfully updated" + } + }, + "inheritanceEstateRecovery": { + "title": "Estate recovery", + "instructions": { + "name": "Clear Device Data", + "dialogs": { + "settings": { + "title": "From the Main Menu of your X1 Vault, click \"Settings\"" + }, + "clearData": { + "title": "Click \"Clear Device Data\" on your X1 Vault" + }, + "confirmClearData": { + "title": "Confirm \"Clear device data\" on the X1 Vault" + }, + "tapCards": { + "title": "Tap any 2 old X1 Cards below the X1 Vault", + "subTitle": "Do not lift until you hear a beep sound", + "messageBoxList": [ + { + "warning": "Tap the cards you used during the initial Cypherock setup process, not the cards given by the wallet owner" + }, + { + "warning": "Your device will restart after this process" + } + ] + } + } + }, + "wallet": { + "name": "Wallet", + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Tap the card you received as a nominee" + }, + "messageBox": { + "warning": "Do not lift until you hear 2 beep sound" + }, + "verification": { + "title": "OTP verification" + }, + "syncing": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + } + }, + "decryption": { + "name": "Decryption", + "device": { + "title": "Follow instructions on the X1 Vault", + "subTitle": "Do not lift the card till you hear a beep sound", + "actions": { + "confirm": "Confirm decryption on device", + "tapCard": "Tap the card you received as a nominee" + }, + "messageBox": { + "warning": "The message will be decrypted by the card you received as a nominee" + } + }, + "error": { + "title": "Decrypted message", + "message": "Update was unsuccessful. Wallet ABC was not found in the card" + } + }, + "viewPin": { + "name": "View PIN", + "title": "Follow instructions on the X1 Vault", + "subTitle": "PIN for wallet", + "actions": { + "view": "View & copy the PIN displayed on your device and then confirm" + }, + "messageBox": { + "warning": "You will need this PIN to access crypto assets on this wallet. Make sure you do not forget it." + } + }, + "viewMessage": { + "name": "View Message", + "title": "Message from ${name}", + "tooltip": "These messages were written by the wallet owner for you. They contain important information about how to recover their crypto assets stored on the Cypherock X1. \nNote- Make sure you keep these messages safe and private. ", + "form": { + "cardLocationField": { + "label": "Card location", + "placeholder": "Write the location of the card", + "tooltip": "This is the location of another card written by wallet owner for you to fetch. You will only be able to recover the crypto assets after getting another card" + }, + "personalMessageField": { + "label": "Personal message", + "placeholder": "Write your message", + "tooltip": "This includes a personal message written by wallet owner for you" + }, + "checkBox": { + "label": "I have read and copied the above messages" + } + } + }, + "confirmation": { + "name": "Confirmation", + "success": { + "title": "Congratulations, you just decrypted the message from ", + "subTitle": "Watch the video to understand the next steps" + } + } } }, "toggle": { @@ -1579,5 +2052,555 @@ } } } + }, + "otp": { + "title": "Enter OTP", + "wrongOtpTitle": "Wrong OTP", + "successRedirectTitle": "Please wait while we redirect you...", + "triesRemaining": "${retries} tries remaining", + "infoTexts": [ + "An email has been sent to **${email}**", + "Please enter the code received on the email" + ], + "noRetries": { + "title": "You have 0 tries remaining", + "subTitle": "Please exit the flow and restart" + }, + "buttons": { + "resendWithTimeout": "Resend OTP in ${timeout}s" + } + }, + "inheritance": { + "title": "Cypherock Cover", + "plans": { + "silver": { + "title": "Silver", + "description": "Secure your wallet PIN with us and easily recover it if forgotten, ensuring continuous access to your crypto assets" + }, + "gold": { + "title": "Gold", + "description": "Ensure your crypto assets are inherited by loved ones effortlessly, bypassing complex legal restrictions for smooth transfers" + } + }, + "choosePlan": { + "title": "Choose a plan", + "plans": { + "buttonText": "SELECT", + "popularTagText": "MOST POPULAR", + "features": [ + "No KYC required", + "Multi-Chain", + "PIN Recovery", + "Estate Recovery", + "Passphrase Recovery" + ] + } + }, + "homePage": { + "headers": { + "owner": { + "title": "Owner", + "subtitle": "Setup recovery for your wallets" + }, + "nominee": { + "title": "Nominee", + "subtitle": "You are a nominee of the following wallets" + } + }, + "setup": { + "setupCover": { + "title": "Setup Cypherock Cover", + "subTitle": "Protect your crypto assets and ensure smooth transfer to your beneficiaries in unforeseen circumstances" + }, + "syncFromMail": { + "title": "Sync from Email", + "subTitle": "If you're already part of a Cypherock Cover plan, either as the plan owner or a beneficiary" + }, + "learnMore": { + "title": "What is Cypherock Cover?", + "subTitle": "Learn how Cypherock Cover secures your crypto assets and ensures their seamless transfer to your loved ones" + } + } + }, + "planDetails": { + "walletDetails": { + "createdOn": "Created on", + "expiringOn": "Expiring on", + "reminderPeriodField": { + "label": "Reminder Period", + "input": "Every ${month} month", + "inputPlural": "Every ${month} months" + } + }, + "ownerDetails": { + "title": "Owner Details", + "form": { + "userNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + } + } + }, + "nomineeDetails": { + "title": "Nominee #${number}", + "form": { + "nomineeNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "encryptedMessage": { + "label": "Encrypted Message" + } + } + }, + "executorDetails": { + "title": "Executor", + "form": { + "nomineeNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "executorMessage": { + "label": "Executor Message" + }, + "assignTo": { + "label": "Assigned to" + } + } + } + }, + "buttons": { + "syncFromEmail": "Sync from Email", + "recoverPin": "Recover PIN", + "renewPlan": "Renew Plan", + "unlock": "Unlock", + "upgradePlan": "Upgrade Plan" + }, + "termsOfService": { + "title": "Terms of Use", + "termsOfService": "Terms of Service", + "privacyPolicy": "Privacy Policy", + "checkBoxLabel": "I have read and agree to Terms of service and Privacy policy" + }, + "dialog": { + "userDetails": { + "form": { + "name": "Name", + "emailField": { + "label": "Primary Email", + "tooltip": "Enter the email on which you want to receive all reminder emails" + }, + "alternateEmail": "Alternate Email" + }, + "error": { + "sameEmail": "Alternate email cannot be same as primary email" + } + }, + "verifyOTP": { + "primaryEmailOTP": { + "title": "OTP Verification for primary email" + }, + "alternateEmailOTP": { + "title": "OTP Verification for alternate email" + } + }, + "payment": { + "heading": "Buy the plan on the Cypherock website", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase", + "form": { + "promoField": { + "label": "Promo code / coupon", + "placeholder": "Enter code" + } + }, + "couponInput": { + "appliedButtonText": "Coupon applied:", + "applyButtonText": "Apply" + }, + "noOfYear": "Number of years", + "total": "Total", + "year": "year", + "error": { + "errorHeading": "Invalid Coupon Code", + "subtext": "Please try a different coupon code" + } + } + }, + "banner": { + "title": "Secure your crypto legacy with Cypherock Cover - Ensure your assets reach your loved ones", + "buttons": { + "knowMore": "Know More" + } + } + }, + "inheritanceSilverPlanPurchase": { + "title": "Silver Plan Setup", + "instructions": { + "heading": "Instructions", + "ensure": { + "title": "Ensure the following before you continue", + "instructions": [ + "Make sure you have 15-30 minutes to set it up", + "You are present in a safe environment", + "You need to have one device and one card handy to set it up" + ] + }, + "video": { + "title": "Watch the tutorial video before you proceed further" + } + }, + "wallet": { + "heading": "Wallet", + "selectWallet": { + "title": "Select the wallet you want to cover in this plan", + "subTitle": "For setting up additional wallets individual plans can be created later", + "tooltip": "This wallet is not having a PIN setup, create a new wallet or select a different wallet having PIN setup" + }, + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 3 beep sounds" + } + } + }, + "email": { + "heading": "Owner", + "userDetails": { + "title": "Your Details", + "subTitle": "All the fields are mandatory", + "buttons": { + "sendOTP": "Send OTP" + } + } + }, + "encryption": { + "heading": "Encryption", + "device": { + "title": "Follow instructions on the X1 Vault to encrypt the PIN", + "subTitle": "Lift your card after 2 beep sounds", + "tooltip": "Tap any one X1 card below the X1 device to encrypt the message and do not remove the card until you hear 2 beep sounds", + "actions": { + "confirm": "Confirm encryption on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The PIN will be encrypted by the X1 card" + } + }, + "loading": { + "title": "Please wait while we save the encrypted data on the server", + "subTitle": "Do not disconnect or close the app" + }, + "success": { + "title": "PIN is successfully encrypted and saved", + "subTitle": "To complete your silver plan setup, you can now proceed with the plan purchase" + } + }, + "checkout": { + "heading": "Checkout", + "payment": { + "title": "Complete your payment", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase" + }, + "success": { + "title": "Thank you!", + "subtext": "Your Silver Plan is now activated" + } + } + }, + "inheritanceGoldPlanPurchase": { + "title": "Gold Plan Setup", + "instructions": { + "heading": "Instructions", + "ensure": { + "title": "Ensure the following before you continue", + "instructions": [ + "Make sure you have 15-30 minutes to set it up", + "You are present in a safe environment", + "You need to have one device and one card handy to set it up" + ] + }, + "video": { + "title": "Watch the tutorial video before you proceed", + "subTitle": "The video will help you take the right decisions" + } + }, + "wallet": { + "heading": "Wallet", + "selectWallet": { + "title": "Select the wallet you want to cover in this plan", + "subTitle": "For setting up additional wallets, individual plans can be created later", + "tooltip": "This wallet is not having a PIN setup, create a new wallet or select a different wallet having PIN setup" + }, + "walletAuth": { + "heading": "Wallet Authentication", + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirmOnDevice": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 3 beep sounds" + }, + "error": { + "title": "Wallet Authentication failed", + "subtext": "We could not authenticate the selected wallet", + "messageBox": { + "warning": "This usually happens when the wallets in the Cysync and the device are not in sync. Sync the wallets between the Cysync and the device or choose a different wallet" + } + } + } + }, + "email": { + "heading": "Owner", + "userDetails": { + "title": "Your Details", + "form": { + "name": "Name", + "emailField": { + "label": "Primary Email", + "tooltip": "Enter the email on which you want to receive all reminder emails" + }, + "alternateEmail": "Alternate Email" + }, + "buttons": { + "sendOTP": "Send OTP" + } + }, + "primaryEmailOTP": { + "title": "OTP Verification for primary email" + }, + "alternateEmailOTP": { + "title": "OTP Verification for alternate email" + } + }, + "nomineeAndExecutor": { + "heading": "Nominees & Executor", + "select": { + "title": "How many nominees do you want?", + "subtitle": "You can have up to 2 nominees for this will", + "tooltip": "In case if you select two nominees then the 2nd nominee will receive the recovery email three months after the 1st nominee receives it\nNote - Both nominees will receive the same recovery email and same message written by you on the CySync app", + "options": { + "descOne": "Select if you want to have only one nominee", + "descTwo": "Select if you want to have two nominees" + } + }, + "nomineeDetails": { + "first": { + "title": "Add first nominee", + "warning": "Make sure you give a card to your nominee", + "tooltip": "Your Nominee will receive recovery instructions on this email" + }, + "second": { + "title": "Add second nominee", + "warning": "Make sure you give a card to your second nominee" + }, + "confirm": { + "title": "Do you want to verify nominee emails?", + "subtext": "An OTP will be sent to the provided emails. If you do not want to inform the nominee about this plan, you can skip verification of emails.", + "warning": "If you do not verify the nominee email then Cypherock emails may land in spam folder" + } + }, + "executor": { + "select": { + "title": "Do you want to have an executor for this will?", + "subtext": "The executor will help the nominee to recover funds ", + "tooltip": "An executor/helper can be someone you trust who does not have any financial stake but can be assigned as a helper to the nominee for funds recovery\nThe executor will receive the email on the same time as the nominee.", + "options": { + "one": { + "yes": "Yes", + "desc": "A tech savvy person whom you trust" + }, + "two": { + "no": "No", + "desc": "If your nominees are tech savvy" + } + } + }, + "executorDetails": { + "title": "Add executor", + "tooltip": "Executor will receive instructions to help nominee on this email", + "radio": { + "label": "Choose a nominee to link with executor", + "tooltip": "Select the nominee you want to link with the executor. If you choose Nominee 1 then the executor will receive their email written by you on same time as Nominee 1.\n\nIf you choose Nominee 2 then the executor will receive their email written by you on same time as Nominee 2 i.e, 3 months after Nominee 1.", + "options": { + "labelPrefix": "Nominee " + } + } + } + } + }, + "message": { + "heading": "Message", + "tutorial": { + "title": "Watch the tutorial video before you proceed further", + "subtext": "How to write the final messages" + }, + "nominee": { + "title": "Draft the final message for the nominee", + "subtitle": "The nominee messages will be end-to-end encrypted", + "tooltip": "End-to-end encryption keeps your messages between you and the nominee you choose. Not even Cypherock can read them", + "form": { + "locationField": { + "label": "Card Location & Passphrase", + "placeholder": "Write the location of the card", + "tooltip": "Write down the location of another card for the nominee to fetch. Nominee will only be able to recover crypto assets after getting another card. Remember this card should be different than the card(s) you may have given to your nominee(s).\nYou can also include the passphrase in this message." + }, + "personalMessage": { + "label": "Personal Message", + "placeholder": "Write your message", + "tooltip": "This can include your will/message for the nominee and other important pieces of information that you may want your nominee to know such as your crypto exchange account details, coins you have amongst others." + } + }, + "messageBox": { + "warning": "Make sure you describe the card's location clearly for the nominee" + } + }, + "executor": { + "title": "Add a message for the executor", + "subtitle": "Please note that the message to the executor is not encrypted", + "tooltip": "This message will be received by the executor in case recovery is triggered, it's recommended to NOT add any critical information here", + "form": { + "messageField": { + "label": "Message", + "placeholder": "Write your message" + } + } + } + }, + "reminder": { + "heading": "Reminder", + "title": "Reminder setup" + }, + "summary": { + "heading": "Summary", + "title": "Summary", + "subtitle": "Please verify all the details before it gets encrypted with the device", + "ownerDetails": { + "title": "Owner Details", + "form": { + "userNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "reminderPeriodField": { + "label": "Reminder Period", + "input": "Every ${month} month", + "inputPlural": "Every ${month} months" + } + } + }, + "nomineeDetails": { + "title": "Nominee #", + "form": { + "nomineeNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + } + } + }, + "cardLocation": { + "title": "Card Location & Passphrase" + }, + "personalMessage": { + "title": "Personal Message" + }, + "executorDetails": { + "title": "Executor", + "form": { + "nomineeNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "assignTo": { + "label": "Assigned to" + } + } + }, + "executorMessage": { + "title": "Executor Message" + } + }, + "encryption": { + "heading": "Encryption", + "confirm": { + "title": "Follow instructions on the X1 Vault to verify the nominee message", + "subtext": "You can encrypt the message in the next step", + "actions": { + "confirmOnDevice": "Confirm message verification on device", + "verifyLocation": "Verify Card Location in your device" + }, + "messageBox": { + "danger": "Read through the whole text and verify that the message shown in the device is same as you entered in cySync" + } + }, + "device": { + "title": "Follow instructions on the X1 Vault to encrypt the wallet PIN", + "subtext": "Lift your card after 2 beep sounds ", + "tooltip": "Tap any one X1 card below the X1 device to encrypt the message and do not remove the card until you hear 2 beep sounds", + "actions": { + "confirmOnDevice": "Confirm encryption on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The PIN will be encrypted by the X1 card" + } + }, + "loading": { + "title": "Please wait while we send the encrypted data to the server", + "subTitle": "Do not disconnect or close the app" + }, + "success": { + "title": "Nominee message successfully encrypted", + "subtext": "To complete your gold plan setup, you can now proceed with the plan purchase" + } + }, + "checkout": { + "heading": "Checkout", + "payment": { + "title": "Complete your payment", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase" + }, + "greeting": { + "title": "Thank you!", + "subtext": "Your Gold Plan is now activated" + } + } } } diff --git a/packages/cysync-core-constants/src/i18n/lang/en.json b/packages/cysync-core-constants/src/i18n/lang/en.json index d6dcd25f3..88f03b0f9 100644 --- a/packages/cysync-core-constants/src/i18n/lang/en.json +++ b/packages/cysync-core-constants/src/i18n/lang/en.json @@ -3,6 +3,10 @@ "help": "Help", "back": "Back", "allWallets": "All Wallets", + "labels": { + "email": "Email", + "required": "Required" + }, "buttons": { "addWallet": "Add Wallet", "addAccount": "Add Account", @@ -40,7 +44,37 @@ "showQRCode": "Show QR Code", "editAccount": "Edit Account", "submit": "Submit", - "showMore": "Show more" + "showMore": "Show more", + "resendOTP": "Resend OTP in ${timeout}s", + "next": "Next", + "setup": "Setup", + "sync": "Sync", + "learnMore": "Learn More", + "checkout": "Checkout", + "yes": "Yes", + "no": "No", + "saveAndContinue": "Save and continue", + "exitWithoutSaving": "Exit without saving", + "saveChanges": "Save changes", + "edit": "Edit", + "tryAgain": "Try Again" + }, + "dashboard": { + "wallet": { + "renewNow": "Renew Now", + "buyNow": "Buy Now", + "created": "Created", + "expiredOn": "Expired on", + "expiresIn": "Expires in", + "expiry": "Expiry", + "expiring": "Expiring", + "expired": "Expired", + "pending": "Pending", + "silver": "Silver", + "gold": "Gold", + "hours": "Hours", + "setupCover": "Setup Cypherock Cover" + } }, "tooltips": { "downloadCsv": "Export as CSV" @@ -618,9 +652,11 @@ "receiveCrypto": "Receive Crypto", "history": "History", "walletConnect": "Wallet Connect", + "cypherockCover": "Cypherock Cover", "tutorial": "Tutorial", "settings": "Settings", "help": "Help", + "new": "New", "tooltip": { "walletDeleted": "${walletName} has been deleted from your Cypherock X1 Vault" } @@ -917,6 +953,9 @@ "heading": "Your X1 Vault has timed-out", "subtext": "Navigate to the main menu on the device and try again" }, + "APP_0207": { + "heading": "Could not establish session on device" + }, "APP_0400": { "heading": "Unknown X1 Card error", "subtext": "Retry or click Help to find a solution" @@ -1028,12 +1067,113 @@ "serverErrors": { "SER_0000": { "heading": "Your cySync app is facing some connectivity issue", - "subtext": "Reconnect the device and try again and if the problem persists, contact Cypherock support for assistance" + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" }, "SER_0001": { "heading": "Please connect to the internet to continue" + }, + "SER_1001": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1003": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1004": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1005": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1006": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1007": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1008": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1009": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1010": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1011": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1012": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1013": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1014": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1015": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1016": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1017": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1018": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1019": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1020": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1021": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1022": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1023": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1024": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1025": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1026": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" } }, + "videoPlaybackError": "No Internet connection. Please check your connection and ", "serverCoinErrors": { "default": { "DEF_0000": { @@ -1046,7 +1186,8 @@ }, "validation": { "generic": { - "required": "This field is required" + "required": "This field is required", + "englishOnly": "Only ASCII characters and English text are allowed" }, "email": { "invalid": "This is not a valid email" @@ -1477,6 +1618,338 @@ "info": "This is a ${derivationSchemeName} xpub. When importing it into a third-party wallet, choose the same derivation mode", "advanced": "Advanced" } + }, + "inheritanceSyncPlans": { + "enterEmail": { + "title": "Please enter your primary email associated with this plan", + "subTitle": "An OTP will be sent to this email", + "button": "Verify" + }, + "verifyEmail": { + "title": "OTP verification" + } + }, + "inheritancePlanLogin": { + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + } + }, + "fetchData": { + "title": "Please wait while we fetch the plan data", + "subTitle": "Do not disconnect or close the app" + }, + "verifyEmail": { + "title": "OTP Verification" + } + }, + "inheritanceEditExecutorMessage": { + "fetchData": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "editMessage": { + "title": "Executor Message", + "form": { + "messageField": { + "label": "Message", + "placeholder": "Executor Message" + } + }, + "buttons": { + "exit": "Exit without saving", + "save": "Save changes" + }, + "loading": { + "title": "Please wait while we update the data", + "subtitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Executor message successfully encrypted" + } + }, + "inheritanceEditReminderTime": { + "fetchData": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "reminderSetup": { + "title": "Reminder setup", + "reminderInfo": { + "subtitle": "What is reminder time?", + "subtext": "Reminder Time is the interval at which you'll receive a confirmation email from Cypherock to verify your continued well-being. If no response is received within two months of the initial email, the inheritance process will commence and your nominee will receive the recovery instruction email from Cypherock" + }, + "form": { + "reminderField": { + "label": "Set reminder time interval", + "month": "Month", + "months": "Months" + } + }, + "currentReminder": "The current reminder is set at ${month}", + "loading": { + "title": "Please wait while we update the data", + "subtitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Reminders duration updated successfully" + } + }, + "inheritanceEditUserDetails": { + "editDetails": { + "title": "Edit ${userType} Details", + "userTypes": { + "owner": "Owner", + "nominee": "Nominee", + "executor": "Executor" + }, + "buttons": { + "verifyEmail": "Verify Email" + } + }, + "verifyOtp": { + "loading": { + "title": "Please wait while we update the data", + "subtext": "Do not disconnect or close the app" + } + }, + "success": { + "title": "${userType} details successfully updated" + } + }, + "inheritancePinRecovery": { + "title": "PIN Recovery", + "sync": { + "name": "Syncing", + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirmAuth": "Confirm wallet authentication on device", + "tapCard": "Tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 2 beep sounds" + } + }, + "verifyOtp": { + "title": "OTP verification" + }, + "fetch": { + "title": "Please wait while we fetch the encrypted PIN", + "subTitle": "Do no disconnect or close the app" + } + }, + "decryptPin": { + "name": "Decryption", + "title": "Follow instructions on the X1 Vault", + "actions": { + "confirmOnDevice": "Confirm decryption on device", + "tapCard": "Tap any one card" + }, + "messageBox": { + "warning": "The PIN will be decrypted by the card" + }, + "error": { + "title": "PIN Decryption Failed", + "subTitle": "PIN decryption was unsuccessful. Wallet ${wallet} was not found in the card" + } + }, + "viewPin": { + "name": "View PIN", + "title": "Follow instructions on the X1 Vault", + "subTitle": "PIN for wallet ", + "actions": { + "viewDevice": "View the PIN on your device and confirm" + } + }, + "success": { + "name": "Confirmation", + "title": "Congratulations, Pin recovery is complete" + } + }, + "inheritanceEditEncryptedMessage": { + "confirmation": { + "title": "Are you sure you want to edit the encrypted nominee message?", + "subTitle": "You will need the device and at least one card to encrypt the message again after editing" + }, + "syncing": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "decryption": { + "wallet": { + "title": "Follow instructions on the X1 Vault", + "actions": { + "confirm": "Confirm decryption on device", + "tapCard": "Tap any card" + }, + "messageBox": { + "warning": "The message will be decrypted by the X1 card" + }, + "decryption": { + "title": "Please wait while we decrypt the data", + "subTitle": "Do not disconnect or close the app" + } + } + }, + "editMessage": { + "title": "Nominee message", + "subTitle": "The nominee messages will be end-to-end encrypted", + "form": { + "cardLocationField": { + "label": "Card location & Passphrase", + "placeholder": "Write the location of the card", + "tooltip": "Write down the location of another card for the nominee to fetch. Nominee will only be able to recover crypto assets after getting another card. Remember this card should be different than the card(s) you may have given to your nominee(s).\nYou can also include the passphrase in this message" + }, + "personalMessageField": { + "label": "Personal message", + "placeholder": "Write your message", + "tooltip": "This can include your will/message for the nominee and other important pieces of information that you may want your nominee to know such as your crypto exchange account details, coins you have amongst others." + } + }, + "messageBox": { + "warning": "Make sure you describe the card's location clearly for the nominee" + } + }, + "confirmMessage": { + "title": "Follow instructions on the X1 Vault to verify the nominee message", + "subTitle": "You can encrypt the message in the next step", + "actions": { + "confirmOnDevice": "Confirm on device", + "verifyLocation": "Verify Card location in your device" + }, + "messageBox": { + "danger": "Read through the whole text and verify that the message shown in the device is same as you entered in cySync" + } + }, + "encryption": { + "title": "Follow instructions on the X1 Vault to encrypt the nominee message", + "subTitle": "Lift your card after 2 beep sounds", + "tooltip": "", + "actions": { + "enterPinAndTap": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The nominee message will be encrypted by the Card" + }, + "syncing": { + "title": "Please wait while we save the encrypted data on the server", + "subTitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Nominee message successfully updated" + } + }, + "inheritanceEstateRecovery": { + "title": "Estate recovery", + "instructions": { + "name": "Clear Device Data", + "dialogs": { + "settings": { + "title": "From the Main Menu of your X1 Vault, click \"Settings\"" + }, + "clearData": { + "title": "Click \"Clear Device Data\" on your X1 Vault" + }, + "confirmClearData": { + "title": "Confirm \"Clear device data\" on the X1 Vault" + }, + "tapCards": { + "title": "Tap any 2 old X1 Cards below the X1 Vault", + "subTitle": "Do not lift until you hear a beep sound", + "messageBoxList": [ + { + "warning": "Tap the cards you used during the initial Cypherock setup process, not the cards given by the wallet owner" + }, + { + "warning": "Your device will restart after this process" + } + ] + } + } + }, + "wallet": { + "name": "Wallet", + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Tap the card you received as a nominee" + }, + "messageBox": { + "warning": "Do not lift until you hear 2 beep sound" + }, + "verification": { + "title": "OTP verification" + }, + "syncing": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + } + }, + "decryption": { + "name": "Decryption", + "device": { + "title": "Follow instructions on the X1 Vault", + "subTitle": "Do not lift the card till you hear a beep sound", + "actions": { + "confirm": "Confirm decryption on device", + "tapCard": "Tap the card you received as a nominee" + }, + "messageBox": { + "warning": "The message will be decrypted by the card you received as a nominee" + } + }, + "error": { + "title": "Decrypted message", + "message": "Update was unsuccessful. Wallet ABC was not found in the card" + } + }, + "viewPin": { + "name": "View PIN", + "title": "Follow instructions on the X1 Vault", + "subTitle": "PIN for wallet", + "actions": { + "view": "View & copy the PIN displayed on your device and then confirm" + }, + "messageBox": { + "warning": "You will need this PIN to access crypto assets on this wallet. Make sure you do not forget it." + } + }, + "viewMessage": { + "name": "View Message", + "title": "Message from ${name}", + "tooltip": "These messages were written by the wallet owner for you. They contain important information about how to recover their crypto assets stored on the Cypherock X1. \nNote- Make sure you keep these messages safe and private. ", + "form": { + "cardLocationField": { + "label": "Card location", + "placeholder": "Write the location of the card", + "tooltip": "This is the location of another card written by wallet owner for you to fetch. You will only be able to recover the crypto assets after getting another card" + }, + "personalMessageField": { + "label": "Personal message", + "placeholder": "Write your message", + "tooltip": "This includes a personal message written by wallet owner for you" + }, + "checkBox": { + "label": "I have read and copied the above messages" + } + } + }, + "confirmation": { + "name": "Confirmation", + "success": { + "title": "Congratulations, you just decrypted the message from ", + "subTitle": "Watch the video to understand the next steps" + } + } } }, "toggle": { @@ -1579,5 +2052,555 @@ } } } + }, + "otp": { + "title": "Enter OTP", + "wrongOtpTitle": "Wrong OTP", + "successRedirectTitle": "Please wait while we redirect you...", + "triesRemaining": "${retries} tries remaining", + "infoTexts": [ + "An email has been sent to **${email}**", + "Please enter the code received on the email" + ], + "noRetries": { + "title": "You have 0 tries remaining", + "subTitle": "Please exit the flow and restart" + }, + "buttons": { + "resendWithTimeout": "Resend OTP in ${timeout}s" + } + }, + "inheritance": { + "title": "Cypherock Cover", + "plans": { + "silver": { + "title": "Silver", + "description": "Secure your wallet PIN with us and easily recover it if forgotten, ensuring continuous access to your crypto assets" + }, + "gold": { + "title": "Gold", + "description": "Ensure your crypto assets are inherited by loved ones effortlessly, bypassing complex legal restrictions for smooth transfers" + } + }, + "choosePlan": { + "title": "Choose a plan", + "plans": { + "buttonText": "SELECT", + "popularTagText": "MOST POPULAR", + "features": [ + "No KYC required", + "Multi-Chain", + "PIN Recovery", + "Estate Recovery", + "Passphrase Recovery" + ] + } + }, + "homePage": { + "headers": { + "owner": { + "title": "Owner", + "subtitle": "Setup recovery for your wallets" + }, + "nominee": { + "title": "Nominee", + "subtitle": "You are a nominee of the following wallets" + } + }, + "setup": { + "setupCover": { + "title": "Setup Cypherock Cover", + "subTitle": "Protect your crypto assets and ensure smooth transfer to your beneficiaries in unforeseen circumstances" + }, + "syncFromMail": { + "title": "Sync from Email", + "subTitle": "If you're already part of a Cypherock Cover plan, either as the plan owner or a beneficiary" + }, + "learnMore": { + "title": "What is Cypherock Cover?", + "subTitle": "Learn how Cypherock Cover secures your crypto assets and ensures their seamless transfer to your loved ones" + } + } + }, + "planDetails": { + "walletDetails": { + "createdOn": "Created on", + "expiringOn": "Expiring on", + "reminderPeriodField": { + "label": "Reminder Period", + "input": "Every ${month} month", + "inputPlural": "Every ${month} months" + } + }, + "ownerDetails": { + "title": "Owner Details", + "form": { + "userNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + } + } + }, + "nomineeDetails": { + "title": "Nominee #${number}", + "form": { + "nomineeNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "encryptedMessage": { + "label": "Encrypted Message" + } + } + }, + "executorDetails": { + "title": "Executor", + "form": { + "nomineeNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "executorMessage": { + "label": "Executor Message" + }, + "assignTo": { + "label": "Assigned to" + } + } + } + }, + "buttons": { + "syncFromEmail": "Sync from Email", + "recoverPin": "Recover PIN", + "renewPlan": "Renew Plan", + "unlock": "Unlock", + "upgradePlan": "Upgrade Plan" + }, + "termsOfService": { + "title": "Terms of Use", + "termsOfService": "Terms of Service", + "privacyPolicy": "Privacy Policy", + "checkBoxLabel": "I have read and agree to Terms of service and Privacy policy" + }, + "dialog": { + "userDetails": { + "form": { + "name": "Name", + "emailField": { + "label": "Primary Email", + "tooltip": "Enter the email on which you want to receive all reminder emails" + }, + "alternateEmail": "Alternate Email" + }, + "error": { + "sameEmail": "Alternate email cannot be same as primary email" + } + }, + "verifyOTP": { + "primaryEmailOTP": { + "title": "OTP Verification for primary email" + }, + "alternateEmailOTP": { + "title": "OTP Verification for alternate email" + } + }, + "payment": { + "heading": "Buy the plan on the Cypherock website", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase", + "form": { + "promoField": { + "label": "Promo code / coupon", + "placeholder": "Enter code" + } + }, + "couponInput": { + "appliedButtonText": "Coupon applied:", + "applyButtonText": "Apply" + }, + "noOfYear": "Number of years", + "total": "Total", + "year": "year", + "error": { + "errorHeading": "Invalid Coupon Code", + "subtext": "Please try a different coupon code" + } + } + }, + "banner": { + "title": "Secure your crypto legacy with Cypherock Cover - Ensure your assets reach your loved ones", + "buttons": { + "knowMore": "Know More" + } + } + }, + "inheritanceSilverPlanPurchase": { + "title": "Silver Plan Setup", + "instructions": { + "heading": "Instructions", + "ensure": { + "title": "Ensure the following before you continue", + "instructions": [ + "Make sure you have 15-30 minutes to set it up", + "You are present in a safe environment", + "You need to have one device and one card handy to set it up" + ] + }, + "video": { + "title": "Watch the tutorial video before you proceed further" + } + }, + "wallet": { + "heading": "Wallet", + "selectWallet": { + "title": "Select the wallet you want to cover in this plan", + "subTitle": "For setting up additional wallets individual plans can be created later", + "tooltip": "This wallet is not having a PIN setup, create a new wallet or select a different wallet having PIN setup" + }, + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 3 beep sounds" + } + } + }, + "email": { + "heading": "Owner", + "userDetails": { + "title": "Your Details", + "subTitle": "All the fields are mandatory", + "buttons": { + "sendOTP": "Send OTP" + } + } + }, + "encryption": { + "heading": "Encryption", + "device": { + "title": "Follow instructions on the X1 Vault to encrypt the PIN", + "subTitle": "Lift your card after 2 beep sounds", + "tooltip": "Tap any one X1 card below the X1 device to encrypt the message and do not remove the card until you hear 2 beep sounds", + "actions": { + "confirm": "Confirm encryption on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The PIN will be encrypted by the X1 card" + } + }, + "loading": { + "title": "Please wait while we save the encrypted data on the server", + "subTitle": "Do not disconnect or close the app" + }, + "success": { + "title": "PIN is successfully encrypted and saved", + "subTitle": "To complete your silver plan setup, you can now proceed with the plan purchase" + } + }, + "checkout": { + "heading": "Checkout", + "payment": { + "title": "Complete your payment", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase" + }, + "success": { + "title": "Thank you!", + "subtext": "Your Silver Plan is now activated" + } + } + }, + "inheritanceGoldPlanPurchase": { + "title": "Gold Plan Setup", + "instructions": { + "heading": "Instructions", + "ensure": { + "title": "Ensure the following before you continue", + "instructions": [ + "Make sure you have 15-30 minutes to set it up", + "You are present in a safe environment", + "You need to have one device and one card handy to set it up" + ] + }, + "video": { + "title": "Watch the tutorial video before you proceed", + "subTitle": "The video will help you take the right decisions" + } + }, + "wallet": { + "heading": "Wallet", + "selectWallet": { + "title": "Select the wallet you want to cover in this plan", + "subTitle": "For setting up additional wallets, individual plans can be created later", + "tooltip": "This wallet is not having a PIN setup, create a new wallet or select a different wallet having PIN setup" + }, + "walletAuth": { + "heading": "Wallet Authentication", + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirmOnDevice": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 3 beep sounds" + }, + "error": { + "title": "Wallet Authentication failed", + "subtext": "We could not authenticate the selected wallet", + "messageBox": { + "warning": "This usually happens when the wallets in the Cysync and the device are not in sync. Sync the wallets between the Cysync and the device or choose a different wallet" + } + } + } + }, + "email": { + "heading": "Owner", + "userDetails": { + "title": "Your Details", + "form": { + "name": "Name", + "emailField": { + "label": "Primary Email", + "tooltip": "Enter the email on which you want to receive all reminder emails" + }, + "alternateEmail": "Alternate Email" + }, + "buttons": { + "sendOTP": "Send OTP" + } + }, + "primaryEmailOTP": { + "title": "OTP Verification for primary email" + }, + "alternateEmailOTP": { + "title": "OTP Verification for alternate email" + } + }, + "nomineeAndExecutor": { + "heading": "Nominees & Executor", + "select": { + "title": "How many nominees do you want?", + "subtitle": "You can have up to 2 nominees for this will", + "tooltip": "In case if you select two nominees then the 2nd nominee will receive the recovery email three months after the 1st nominee receives it\nNote - Both nominees will receive the same recovery email and same message written by you on the CySync app", + "options": { + "descOne": "Select if you want to have only one nominee", + "descTwo": "Select if you want to have two nominees" + } + }, + "nomineeDetails": { + "first": { + "title": "Add first nominee", + "warning": "Make sure you give a card to your nominee", + "tooltip": "Your Nominee will receive recovery instructions on this email" + }, + "second": { + "title": "Add second nominee", + "warning": "Make sure you give a card to your second nominee" + }, + "confirm": { + "title": "Do you want to verify nominee emails?", + "subtext": "An OTP will be sent to the provided emails. If you do not want to inform the nominee about this plan, you can skip verification of emails.", + "warning": "If you do not verify the nominee email then Cypherock emails may land in spam folder" + } + }, + "executor": { + "select": { + "title": "Do you want to have an executor for this will?", + "subtext": "The executor will help the nominee to recover funds ", + "tooltip": "An executor/helper can be someone you trust who does not have any financial stake but can be assigned as a helper to the nominee for funds recovery\nThe executor will receive the email on the same time as the nominee.", + "options": { + "one": { + "yes": "Yes", + "desc": "A tech savvy person whom you trust" + }, + "two": { + "no": "No", + "desc": "If your nominees are tech savvy" + } + } + }, + "executorDetails": { + "title": "Add executor", + "tooltip": "Executor will receive instructions to help nominee on this email", + "radio": { + "label": "Choose a nominee to link with executor", + "tooltip": "Select the nominee you want to link with the executor. If you choose Nominee 1 then the executor will receive their email written by you on same time as Nominee 1.\n\nIf you choose Nominee 2 then the executor will receive their email written by you on same time as Nominee 2 i.e, 3 months after Nominee 1.", + "options": { + "labelPrefix": "Nominee " + } + } + } + } + }, + "message": { + "heading": "Message", + "tutorial": { + "title": "Watch the tutorial video before you proceed further", + "subtext": "How to write the final messages" + }, + "nominee": { + "title": "Draft the final message for the nominee", + "subtitle": "The nominee messages will be end-to-end encrypted", + "tooltip": "End-to-end encryption keeps your messages between you and the nominee you choose. Not even Cypherock can read them", + "form": { + "locationField": { + "label": "Card Location & Passphrase", + "placeholder": "Write the location of the card", + "tooltip": "Write down the location of another card for the nominee to fetch. Nominee will only be able to recover crypto assets after getting another card. Remember this card should be different than the card(s) you may have given to your nominee(s).\nYou can also include the passphrase in this message." + }, + "personalMessage": { + "label": "Personal Message", + "placeholder": "Write your message", + "tooltip": "This can include your will/message for the nominee and other important pieces of information that you may want your nominee to know such as your crypto exchange account details, coins you have amongst others." + } + }, + "messageBox": { + "warning": "Make sure you describe the card's location clearly for the nominee" + } + }, + "executor": { + "title": "Add a message for the executor", + "subtitle": "Please note that the message to the executor is not encrypted", + "tooltip": "This message will be received by the executor in case recovery is triggered, it's recommended to NOT add any critical information here", + "form": { + "messageField": { + "label": "Message", + "placeholder": "Write your message" + } + } + } + }, + "reminder": { + "heading": "Reminder", + "title": "Reminder setup" + }, + "summary": { + "heading": "Summary", + "title": "Summary", + "subtitle": "Please verify all the details before it gets encrypted with the device", + "ownerDetails": { + "title": "Owner Details", + "form": { + "userNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "reminderPeriodField": { + "label": "Reminder Period", + "input": "Every ${month} month", + "inputPlural": "Every ${month} months" + } + } + }, + "nomineeDetails": { + "title": "Nominee #", + "form": { + "nomineeNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + } + } + }, + "cardLocation": { + "title": "Card Location & Passphrase" + }, + "personalMessage": { + "title": "Personal Message" + }, + "executorDetails": { + "title": "Executor", + "form": { + "nomineeNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "assignTo": { + "label": "Assigned to" + } + } + }, + "executorMessage": { + "title": "Executor Message" + } + }, + "encryption": { + "heading": "Encryption", + "confirm": { + "title": "Follow instructions on the X1 Vault to verify the nominee message", + "subtext": "You can encrypt the message in the next step", + "actions": { + "confirmOnDevice": "Confirm message verification on device", + "verifyLocation": "Verify Card Location in your device" + }, + "messageBox": { + "danger": "Read through the whole text and verify that the message shown in the device is same as you entered in cySync" + } + }, + "device": { + "title": "Follow instructions on the X1 Vault to encrypt the wallet PIN", + "subtext": "Lift your card after 2 beep sounds ", + "tooltip": "Tap any one X1 card below the X1 device to encrypt the message and do not remove the card until you hear 2 beep sounds", + "actions": { + "confirmOnDevice": "Confirm encryption on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The PIN will be encrypted by the X1 card" + } + }, + "loading": { + "title": "Please wait while we send the encrypted data to the server", + "subTitle": "Do not disconnect or close the app" + }, + "success": { + "title": "Nominee message successfully encrypted", + "subtext": "To complete your gold plan setup, you can now proceed with the plan purchase" + } + }, + "checkout": { + "heading": "Checkout", + "payment": { + "title": "Complete your payment", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase" + }, + "greeting": { + "title": "Thank you!", + "subtext": "Your Gold Plan is now activated" + } + } } } diff --git a/packages/cysync-core-constants/src/i18n/lang/id-ID.json b/packages/cysync-core-constants/src/i18n/lang/id-ID.json index c1f95e733..8d3746d20 100644 --- a/packages/cysync-core-constants/src/i18n/lang/id-ID.json +++ b/packages/cysync-core-constants/src/i18n/lang/id-ID.json @@ -3,6 +3,10 @@ "help": "Help", "back": "Kembali", "allWallets": "Semua Wallets", + "labels": { + "email": "Email", + "required": "Required" + }, "buttons": { "addWallet": "Tambah Dompet", "addAccount": "Tambah Akun", @@ -40,7 +44,37 @@ "showQRCode": "Tampilkan Kode QR", "editAccount": "Sunting Akun", "submit": "Kirim", - "showMore": "Tampilkan lebih banyak" + "showMore": "Tampilkan lebih banyak", + "resendOTP": "Resend OTP in ${timeout}s", + "next": "Next", + "setup": "Setup", + "sync": "Sync", + "learnMore": "Learn More", + "checkout": "Checkout", + "yes": "Yes", + "no": "No", + "saveAndContinue": "Save and continue", + "exitWithoutSaving": "Exit without saving", + "saveChanges": "Save changes", + "edit": "Edit", + "tryAgain": "Try Again" + }, + "dashboard": { + "wallet": { + "renewNow": "Renew Now", + "buyNow": "Buy Now", + "created": "Created", + "expiredOn": "Expired on", + "expiresIn": "Expires in", + "expiry": "Expiry", + "expiring": "Expiring", + "expired": "Expired", + "pending": "Pending", + "silver": "Silver", + "gold": "Gold", + "hours": "Hours", + "setupCover": "Setup Cypherock Cover" + } }, "tooltips": { "downloadCsv": "Ekspor sebagai CSV" @@ -618,9 +652,11 @@ "receiveCrypto": "Terima Crypto", "history": "Riwayat", "walletConnect": "Wallet Connect", + "cypherockCover": "Cypherock Cover", "tutorial": "Tutorial", "settings": "Pengaturan", "help": "Help", + "new": "New", "tooltip": { "walletDeleted": "${walletName} telah dihapus dari Cypherock X1 Vault Anda" } @@ -917,6 +953,9 @@ "heading": "X1 Vault Anda telah kedaluwarsa", "subtext": "Navigasikan ke menu utama di perangkat dan coba lagi" }, + "APP_0207": { + "heading": "Could not establish session on device" + }, "APP_0400": { "heading": "Kesalahan X1 Card tidak dikenal", "subtext": "Coba lagi atau klik Bantuan untuk menemukan solusinya" @@ -1028,12 +1067,113 @@ "serverErrors": { "SER_0000": { "heading": "cySync app Anda menghadapi beberapa masalah konektivitas", - "subtext": "Sambungkan kembali perangkat dan coba lagi dan jika masalah berlanjut, hubungi dukungan Cypherock untuk bantuan" + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" }, "SER_0001": { "heading": "Harap sambungkan ke internet untuk melanjutkan" + }, + "SER_1001": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1003": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1004": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1005": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1006": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1007": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1008": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1009": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1010": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1011": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1012": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1013": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1014": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1015": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1016": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1017": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1018": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1019": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1020": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1021": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1022": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1023": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1024": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1025": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1026": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" } }, + "videoPlaybackError": "No Internet connection. Please check your connection and ", "serverCoinErrors": { "default": { "DEF_0000": { @@ -1046,7 +1186,8 @@ }, "validation": { "generic": { - "required": "Kolom ini wajib diisi" + "required": "Kolom ini wajib diisi", + "englishOnly": "Only ASCII characters and English text are allowed" }, "email": { "invalid": "Ini bukan email yang valid" @@ -1477,6 +1618,338 @@ "info": "Ini adalah ${derivationSchemeName} xpub. Saat mengimpor ke dalam dompet pihak ketiga, pilih mode derivasi yang sama", "advanced": "Lanjutan" } + }, + "inheritanceSyncPlans": { + "enterEmail": { + "title": "Please enter your primary email associated with this plan", + "subTitle": "An OTP will be sent to this email", + "button": "Verify" + }, + "verifyEmail": { + "title": "OTP verification" + } + }, + "inheritancePlanLogin": { + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + } + }, + "fetchData": { + "title": "Please wait while we fetch the plan data", + "subTitle": "Do not disconnect or close the app" + }, + "verifyEmail": { + "title": "OTP Verification" + } + }, + "inheritanceEditExecutorMessage": { + "fetchData": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "editMessage": { + "title": "Executor Message", + "form": { + "messageField": { + "label": "Message", + "placeholder": "Executor Message" + } + }, + "buttons": { + "exit": "Exit without saving", + "save": "Save changes" + }, + "loading": { + "title": "Please wait while we update the data", + "subtitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Executor message successfully encrypted" + } + }, + "inheritanceEditReminderTime": { + "fetchData": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "reminderSetup": { + "title": "Reminder setup", + "reminderInfo": { + "subtitle": "What is reminder time?", + "subtext": "Reminder Time is the interval at which you'll receive a confirmation email from Cypherock to verify your continued well-being. If no response is received within two months of the initial email, the inheritance process will commence and your nominee will receive the recovery instruction email from Cypherock" + }, + "form": { + "reminderField": { + "label": "Set reminder time interval", + "month": "Month", + "months": "Months" + } + }, + "currentReminder": "The current reminder is set at ${month}", + "loading": { + "title": "Please wait while we update the data", + "subtitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Reminders duration updated successfully" + } + }, + "inheritanceEditUserDetails": { + "editDetails": { + "title": "Edit ${userType} Details", + "userTypes": { + "owner": "Owner", + "nominee": "Nominee", + "executor": "Executor" + }, + "buttons": { + "verifyEmail": "Verify Email" + } + }, + "verifyOtp": { + "loading": { + "title": "Please wait while we update the data", + "subtext": "Do not disconnect or close the app" + } + }, + "success": { + "title": "${userType} details successfully updated" + } + }, + "inheritancePinRecovery": { + "title": "PIN Recovery", + "sync": { + "name": "Syncing", + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirmAuth": "Confirm wallet authentication on device", + "tapCard": "Tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 2 beep sounds" + } + }, + "verifyOtp": { + "title": "OTP verification" + }, + "fetch": { + "title": "Please wait while we fetch the encrypted PIN", + "subTitle": "Do no disconnect or close the app" + } + }, + "decryptPin": { + "name": "Decryption", + "title": "Follow instructions on the X1 Vault", + "actions": { + "confirmOnDevice": "Confirm decryption on device", + "tapCard": "Tap any one card" + }, + "messageBox": { + "warning": "The PIN will be decrypted by the card" + }, + "error": { + "title": "PIN Decryption Failed", + "subTitle": "PIN decryption was unsuccessful. Wallet ${wallet} was not found in the card" + } + }, + "viewPin": { + "name": "View PIN", + "title": "Follow instructions on the X1 Vault", + "subTitle": "PIN for wallet ", + "actions": { + "viewDevice": "View the PIN on your device and confirm" + } + }, + "success": { + "name": "Confirmation", + "title": "Congratulations, Pin recovery is complete" + } + }, + "inheritanceEditEncryptedMessage": { + "confirmation": { + "title": "Are you sure you want to edit the encrypted nominee message?", + "subTitle": "You will need the device and at least one card to encrypt the message again after editing" + }, + "syncing": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "decryption": { + "wallet": { + "title": "Follow instructions on the X1 Vault", + "actions": { + "confirm": "Confirm decryption on device", + "tapCard": "Tap any card" + }, + "messageBox": { + "warning": "The message will be decrypted by the X1 card" + }, + "decryption": { + "title": "Please wait while we decrypt the data", + "subTitle": "Do not disconnect or close the app" + } + } + }, + "editMessage": { + "title": "Nominee message", + "subTitle": "The nominee messages will be end-to-end encrypted", + "form": { + "cardLocationField": { + "label": "Card location & Passphrase", + "placeholder": "Write the location of the card", + "tooltip": "Write down the location of another card for the nominee to fetch. Nominee will only be able to recover crypto assets after getting another card. Remember this card should be different than the card(s) you may have given to your nominee(s).\nYou can also include the passphrase in this message" + }, + "personalMessageField": { + "label": "Personal message", + "placeholder": "Write your message", + "tooltip": "This can include your will/message for the nominee and other important pieces of information that you may want your nominee to know such as your crypto exchange account details, coins you have amongst others." + } + }, + "messageBox": { + "warning": "Make sure you describe the card's location clearly for the nominee" + } + }, + "confirmMessage": { + "title": "Follow instructions on the X1 Vault to verify the nominee message", + "subTitle": "You can encrypt the message in the next step", + "actions": { + "confirmOnDevice": "Confirm on device", + "verifyLocation": "Verify Card location in your device" + }, + "messageBox": { + "danger": "Read through the whole text and verify that the message shown in the device is same as you entered in cySync" + } + }, + "encryption": { + "title": "Follow instructions on the X1 Vault to encrypt the nominee message", + "subTitle": "Lift your card after 2 beep sounds", + "tooltip": "", + "actions": { + "enterPinAndTap": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The nominee message will be encrypted by the Card" + }, + "syncing": { + "title": "Please wait while we save the encrypted data on the server", + "subTitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Nominee message successfully updated" + } + }, + "inheritanceEstateRecovery": { + "title": "Estate recovery", + "instructions": { + "name": "Clear Device Data", + "dialogs": { + "settings": { + "title": "From the Main Menu of your X1 Vault, click \"Settings\"" + }, + "clearData": { + "title": "Click \"Clear Device Data\" on your X1 Vault" + }, + "confirmClearData": { + "title": "Confirm \"Clear device data\" on the X1 Vault" + }, + "tapCards": { + "title": "Tap any 2 old X1 Cards below the X1 Vault", + "subTitle": "Do not lift until you hear a beep sound", + "messageBoxList": [ + { + "warning": "Tap the cards you used during the initial Cypherock setup process, not the cards given by the wallet owner" + }, + { + "warning": "Your device will restart after this process" + } + ] + } + } + }, + "wallet": { + "name": "Wallet", + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Tap the card you received as a nominee" + }, + "messageBox": { + "warning": "Do not lift until you hear 2 beep sound" + }, + "verification": { + "title": "OTP verification" + }, + "syncing": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + } + }, + "decryption": { + "name": "Decryption", + "device": { + "title": "Follow instructions on the X1 Vault", + "subTitle": "Do not lift the card till you hear a beep sound", + "actions": { + "confirm": "Confirm decryption on device", + "tapCard": "Tap the card you received as a nominee" + }, + "messageBox": { + "warning": "The message will be decrypted by the card you received as a nominee" + } + }, + "error": { + "title": "Decrypted message", + "message": "Update was unsuccessful. Wallet ABC was not found in the card" + } + }, + "viewPin": { + "name": "View PIN", + "title": "Follow instructions on the X1 Vault", + "subTitle": "PIN for wallet", + "actions": { + "view": "View & copy the PIN displayed on your device and then confirm" + }, + "messageBox": { + "warning": "You will need this PIN to access crypto assets on this wallet. Make sure you do not forget it." + } + }, + "viewMessage": { + "name": "View Message", + "title": "Message from ${name}", + "tooltip": "These messages were written by the wallet owner for you. They contain important information about how to recover their crypto assets stored on the Cypherock X1. \nNote- Make sure you keep these messages safe and private. ", + "form": { + "cardLocationField": { + "label": "Card location", + "placeholder": "Write the location of the card", + "tooltip": "This is the location of another card written by wallet owner for you to fetch. You will only be able to recover the crypto assets after getting another card" + }, + "personalMessageField": { + "label": "Personal message", + "placeholder": "Write your message", + "tooltip": "This includes a personal message written by wallet owner for you" + }, + "checkBox": { + "label": "I have read and copied the above messages" + } + } + }, + "confirmation": { + "name": "Confirmation", + "success": { + "title": "Congratulations, you just decrypted the message from ", + "subTitle": "Watch the video to understand the next steps" + } + } } }, "toggle": { @@ -1579,5 +2052,555 @@ } } } + }, + "otp": { + "title": "Enter OTP", + "wrongOtpTitle": "Wrong OTP", + "successRedirectTitle": "Please wait while we redirect you...", + "triesRemaining": "${retries} tries remaining", + "infoTexts": [ + "An email has been sent to **${email}**", + "Please enter the code received on the email" + ], + "noRetries": { + "title": "You have 0 tries remaining", + "subTitle": "Please exit the flow and restart" + }, + "buttons": { + "resendWithTimeout": "Resend OTP in ${timeout}s" + } + }, + "inheritance": { + "title": "Cypherock Cover", + "plans": { + "silver": { + "title": "Silver", + "description": "Secure your wallet PIN with us and easily recover it if forgotten, ensuring continuous access to your crypto assets" + }, + "gold": { + "title": "Gold", + "description": "Ensure your crypto assets are inherited by loved ones effortlessly, bypassing complex legal restrictions for smooth transfers" + } + }, + "choosePlan": { + "title": "Choose a plan", + "plans": { + "buttonText": "SELECT", + "popularTagText": "MOST POPULAR", + "features": [ + "No KYC required", + "Multi-Chain", + "PIN Recovery", + "Estate Recovery", + "Passphrase Recovery" + ] + } + }, + "homePage": { + "headers": { + "owner": { + "title": "Owner", + "subtitle": "Setup recovery for your wallets" + }, + "nominee": { + "title": "Nominee", + "subtitle": "You are a nominee of the following wallets" + } + }, + "setup": { + "setupCover": { + "title": "Setup Cypherock Cover", + "subTitle": "Protect your crypto assets and ensure smooth transfer to your beneficiaries in unforeseen circumstances" + }, + "syncFromMail": { + "title": "Sync from Email", + "subTitle": "If you're already part of a Cypherock Cover plan, either as the plan owner or a beneficiary" + }, + "learnMore": { + "title": "What is Cypherock Cover?", + "subTitle": "Learn how Cypherock Cover secures your crypto assets and ensures their seamless transfer to your loved ones" + } + } + }, + "planDetails": { + "walletDetails": { + "createdOn": "Created on", + "expiringOn": "Expiring on", + "reminderPeriodField": { + "label": "Reminder Period", + "input": "Every ${month} month", + "inputPlural": "Every ${month} months" + } + }, + "ownerDetails": { + "title": "Owner Details", + "form": { + "userNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + } + } + }, + "nomineeDetails": { + "title": "Nominee #${number}", + "form": { + "nomineeNameField": { + "label": "Nominee Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "encryptedMessage": { + "label": "Encrypted Message" + } + } + }, + "executorDetails": { + "title": "Executor", + "form": { + "nomineeNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "executorMessage": { + "label": "Executor Message" + }, + "assignTo": { + "label": "Assigned to" + } + } + } + }, + "buttons": { + "syncFromEmail": "Sync from Email", + "recoverPin": "Recover PIN", + "renewPlan": "Renew Plan", + "unlock": "Unlock", + "upgradePlan": "Upgrade Plan" + }, + "termsOfService": { + "title": "Terms of Use", + "termsOfService": "Terms of Service", + "privacyPolicy": "Privacy Policy", + "checkBoxLabel": "I have read and agree to Terms of service and Privacy policy" + }, + "dialog": { + "userDetails": { + "form": { + "name": "Name", + "emailField": { + "label": "Primary Email", + "tooltip": "Enter the email on which you want to receive all reminder emails" + }, + "alternateEmail": "Alternate Email" + }, + "error": { + "sameEmail": "Alternate email cannot be same as primary email" + } + }, + "verifyOTP": { + "primaryEmailOTP": { + "title": "OTP Verification for primary email" + }, + "alternateEmailOTP": { + "title": "OTP Verification for alternate email" + } + }, + "payment": { + "heading": "Buy the plan on the Cypherock website", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase", + "form": { + "promoField": { + "label": "Promo code / coupon", + "placeholder": "Enter code" + } + }, + "couponInput": { + "appliedButtonText": "Coupon applied:", + "applyButtonText": "Apply" + }, + "noOfYear": "Number of years", + "total": "Total", + "year": "year", + "error": { + "errorHeading": "Invalid Coupon Code", + "subtext": "Please try a different coupon code" + } + } + }, + "banner": { + "title": "Secure your crypto legacy with Cypherock Cover - Ensure your assets reach your loved ones", + "buttons": { + "knowMore": "Know More" + } + } + }, + "inheritanceSilverPlanPurchase": { + "title": "Silver Plan Setup", + "instructions": { + "heading": "Instructions", + "ensure": { + "title": "Ensure the following before you continue", + "instructions": [ + "Make sure you have 15-30 minutes to set it up", + "You are present in a safe environment", + "You need to have one device and one card handy to set it up" + ] + }, + "video": { + "title": "Watch the tutorial video before you proceed further" + } + }, + "wallet": { + "heading": "Wallet", + "selectWallet": { + "title": "Select the wallet you want to cover in this plan", + "subTitle": "For setting up additional wallets individual plans can be created later", + "tooltip": "This wallet is not having a PIN setup, create a new wallet or select a different wallet having PIN setup" + }, + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 3 beep sounds" + } + } + }, + "email": { + "heading": "Owner", + "userDetails": { + "title": "Your Details", + "subTitle": "All the fields are mandatory", + "buttons": { + "sendOTP": "Send OTP" + } + } + }, + "encryption": { + "heading": "Encryption", + "device": { + "title": "Follow instructions on the X1 Vault to encrypt the PIN", + "subTitle": "Lift your card after 2 beep sounds", + "tooltip": "Tap any one X1 card below the X1 device to encrypt the message and do not remove the card until you hear 2 beep sounds", + "actions": { + "confirm": "Confirm encryption on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The PIN will be encrypted by the X1 card" + } + }, + "loading": { + "title": "Please wait while we save the encrypted data on the server", + "subTitle": "Do not disconnect or close the app" + }, + "success": { + "title": "PIN is successfully encrypted and saved", + "subTitle": "To complete your silver plan setup, you can now proceed with the plan purchase" + } + }, + "checkout": { + "heading": "Checkout", + "payment": { + "title": "Complete your payment", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase" + }, + "success": { + "title": "Thank you!", + "subtext": "Your Silver Plan is now activated" + } + } + }, + "inheritanceGoldPlanPurchase": { + "title": "Gold Plan Setup", + "instructions": { + "heading": "Instructions", + "ensure": { + "title": "Ensure the following before you continue", + "instructions": [ + "Make sure you have 15-30 minutes to set it up", + "You are present in a safe environment", + "You need to have one device and one card handy to set it up" + ] + }, + "video": { + "title": "Watch the tutorial video before you proceed", + "subTitle": "The video will help you take the right decisions" + } + }, + "wallet": { + "heading": "Wallet", + "selectWallet": { + "title": "Select the wallet you want to cover in this plan", + "subTitle": "For setting up additional wallets, individual plans can be created later", + "tooltip": "This wallet is not having a PIN setup, create a new wallet or select a different wallet having PIN setup" + }, + "walletAuth": { + "heading": "Wallet Authentication", + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirmOnDevice": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 3 beep sounds" + }, + "error": { + "title": "Wallet Authentication failed", + "subtext": "We could not authenticate the selected wallet", + "messageBox": { + "warning": "This usually happens when the wallets in the Cysync and the device are not in sync. Sync the wallets between the Cysync and the device or choose a different wallet" + } + } + } + }, + "email": { + "heading": "Owner", + "userDetails": { + "title": "Your Details", + "form": { + "name": "Name", + "emailField": { + "label": "Primary Email", + "tooltip": "Enter the email on which you want to receive all reminder emails" + }, + "alternateEmail": "Alternate Email" + }, + "buttons": { + "sendOTP": "Send OTP" + } + }, + "primaryEmailOTP": { + "title": "OTP Verification for primary email" + }, + "alternateEmailOTP": { + "title": "OTP Verification for alternate email" + } + }, + "nomineeAndExecutor": { + "heading": "Nominees & Executor", + "select": { + "title": "How many nominees do you want?", + "subtitle": "You can have up to 2 nominees for this will", + "tooltip": "In case if you select two nominees then the 2nd nominee will receive the recovery email three months after the 1st nominee receives it\nNote - Both nominees will receive the same recovery email and same message written by you on the CySync app", + "options": { + "descOne": "Select if you want to have only one nominee", + "descTwo": "Select if you want to have two nominees" + } + }, + "nomineeDetails": { + "first": { + "title": "Add first nominee", + "warning": "Make sure you give a card to your nominee", + "tooltip": "Your Nominee will receive recovery instructions on this email" + }, + "second": { + "title": "Add second nominee", + "warning": "Make sure you give a card to your second nominee" + }, + "confirm": { + "title": "Do you want to verify nominee emails?", + "subtext": "An OTP will be sent to the provided emails. If you do not want to inform the nominee about this plan, you can skip verification of emails.", + "warning": "If you do not verify the nominee email then Cypherock emails may land in spam folder" + } + }, + "executor": { + "select": { + "title": "Do you want to have an executor for this will?", + "subtext": "The executor will help the nominee to recover funds ", + "tooltip": "An executor/helper can be someone you trust who does not have any financial stake but can be assigned as a helper to the nominee for funds recovery\nThe executor will receive the email on the same time as the nominee.", + "options": { + "one": { + "yes": "Yes", + "desc": "A tech savvy person whom you trust" + }, + "two": { + "no": "No", + "desc": "If your nominees are tech savvy" + } + } + }, + "executorDetails": { + "title": "Add executor", + "tooltip": "Executor will receive instructions to help nominee on this email", + "radio": { + "label": "Choose a nominee to link with executor", + "tooltip": "Select the nominee you want to link with the executor. If you choose Nominee 1 then the executor will receive their email written by you on same time as Nominee 1.\n\nIf you choose Nominee 2 then the executor will receive their email written by you on same time as Nominee 2 i.e, 3 months after Nominee 1.", + "options": { + "labelPrefix": "Nominee " + } + } + } + } + }, + "message": { + "heading": "Message", + "tutorial": { + "title": "Watch the tutorial video before you proceed further", + "subtext": "How to write the final messages" + }, + "nominee": { + "title": "Draft the final message for the nominee", + "subtitle": "The nominee messages will be end-to-end encrypted", + "tooltip": "End-to-end encryption keeps your messages between you and the nominee you choose. Not even Cypherock can read them", + "form": { + "locationField": { + "label": "Card Location & Passphrase", + "placeholder": "Write the location of the card", + "tooltip": "Write down the location of another card for the nominee to fetch. Nominee will only be able to recover crypto assets after getting another card. Remember this card should be different than the card(s) you may have given to your nominee(s).\nYou can also include the passphrase in this message." + }, + "personalMessage": { + "label": "Personal Message", + "placeholder": "Write your message", + "tooltip": "This can include your will/message for the nominee and other important pieces of information that you may want your nominee to know such as your crypto exchange account details, coins you have amongst others." + } + }, + "messageBox": { + "warning": "Make sure you describe the card's location clearly for the nominee" + } + }, + "executor": { + "title": "Add a message for the executor", + "subtitle": "Please note that the message to the executor is not encrypted", + "tooltip": "This message will be received by the executor in case recovery is triggered, it's recommended to NOT add any critical information here", + "form": { + "messageField": { + "label": "Message", + "placeholder": "Write your message" + } + } + } + }, + "reminder": { + "heading": "Reminder", + "title": "Reminder setup" + }, + "summary": { + "heading": "Summary", + "title": "Summary", + "subtitle": "Please verify all the details before it gets encrypted with the device", + "ownerDetails": { + "title": "Owner Details", + "form": { + "userNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "reminderPeriodField": { + "label": "Reminder Period", + "input": "Every ${month} month", + "inputPlural": "Every ${month} months" + } + } + }, + "nomineeDetails": { + "title": "Nominee #", + "form": { + "nomineeNameField": { + "label": "Nominee Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + } + } + }, + "cardLocation": { + "title": "Card Location & Passphrase" + }, + "personalMessage": { + "title": "Personal Message" + }, + "executorDetails": { + "title": "Executor", + "form": { + "nomineeNameField": { + "label": "Nominee Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "assignTo": { + "label": "Assigned to" + } + } + }, + "executorMessage": { + "title": "Executor Message" + } + }, + "encryption": { + "heading": "Encryption", + "confirm": { + "title": "Follow instructions on the X1 Vault to verify the nominee message", + "subtext": "You can encrypt the message in the next step", + "actions": { + "confirmOnDevice": "Confirm message verification on device", + "verifyLocation": "Verify Card Location in your device" + }, + "messageBox": { + "danger": "Read through the whole text and verify that the message shown in the device is same as you entered in cySync" + } + }, + "device": { + "title": "Follow instructions on the X1 Vault to encrypt the wallet PIN", + "subtext": "Lift your card after 2 beep sounds ", + "tooltip": "Tap any one X1 card below the X1 device to encrypt the message and do not remove the card until you hear 2 beep sounds", + "actions": { + "confirmOnDevice": "Confirm encryption on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The PIN will be encrypted by the X1 card" + } + }, + "loading": { + "title": "Please wait while we send the encrypted data to the server", + "subTitle": "Do not disconnect or close the app" + }, + "success": { + "title": "Nominee message successfully encrypted", + "subtext": "To complete your gold plan setup, you can now proceed with the plan purchase" + } + }, + "checkout": { + "heading": "Checkout", + "payment": { + "title": "Complete your payment", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase" + }, + "greeting": { + "title": "Thank you!", + "subtext": "Your Gold Plan is now activated" + } + } } } diff --git a/packages/cysync-core-constants/src/i18n/lang/zh-CN.json b/packages/cysync-core-constants/src/i18n/lang/zh-CN.json index ded00b4ed..b92364314 100644 --- a/packages/cysync-core-constants/src/i18n/lang/zh-CN.json +++ b/packages/cysync-core-constants/src/i18n/lang/zh-CN.json @@ -3,6 +3,10 @@ "help": "Help", "back": "返回", "allWallets": "所有 Wallets", + "labels": { + "email": "Email", + "required": "Required" + }, "buttons": { "addWallet": "添加 Wallet", "addAccount": "添加账户", @@ -40,7 +44,37 @@ "showQRCode": "显示二维码", "editAccount": "编辑账户", "submit": "提交", - "showMore": "显示更多" + "showMore": "显示更多", + "resendOTP": "Resend OTP in ${timeout}s", + "next": "Next", + "setup": "Setup", + "sync": "Sync", + "learnMore": "Learn More", + "checkout": "Checkout", + "yes": "Yes", + "no": "No", + "saveAndContinue": "Save and continue", + "exitWithoutSaving": "Exit without saving", + "saveChanges": "Save changes", + "edit": "Edit", + "tryAgain": "Try Again" + }, + "dashboard": { + "wallet": { + "renewNow": "Renew Now", + "buyNow": "Buy Now", + "created": "Created", + "expiredOn": "Expired on", + "expiresIn": "Expires in", + "expiry": "Expiry", + "expiring": "Expiring", + "expired": "Expired", + "pending": "Pending", + "silver": "Silver", + "gold": "Gold", + "hours": "Hours", + "setupCover": "Setup Cypherock Cover" + } }, "tooltips": { "downloadCsv": "导出为CSV" @@ -616,9 +650,11 @@ "receiveCrypto": "接收加密货币", "history": "历史", "walletConnect": "Wallet Connect", + "cypherockCover": "Cypherock Cover", "tutorial": "教程", "settings": "设置", "help": "Help", + "new": "New", "tooltip": { "walletDeleted": "${walletName} 已从您的 Cypherock X1 Vault 中删除" } @@ -912,6 +948,9 @@ "heading": "您的 X1 Vault 已超时", "subtext": "在设备上导航到主菜单并重试" }, + "APP_0207": { + "heading": "Could not establish session on device" + }, "APP_0400": { "heading": "未知的 X1 Card 错误", "subtext": "重试或单击 帮助 以找到解决方案" @@ -1023,12 +1062,113 @@ "serverErrors": { "SER_0000": { "heading": "您的 CySync App 遇到了一些连接问题", - "subtext": "重新连接设备重试,如果问题仍然存在,请联系 Cypherock 支持以获取帮助" + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" }, "SER_0001": { "heading": "请连接到互联网以继续" + }, + "SER_1001": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1003": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1004": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1005": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1006": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1007": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1008": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1009": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1010": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1011": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1012": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1013": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1014": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1015": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1016": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1017": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1018": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1019": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1020": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1021": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1022": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1023": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1024": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1025": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" + }, + "SER_1026": { + "heading": "Your cySync app is facing some connectivity issue", + "subtext": "Try again and if the problem persists, contact Cypherock support for assistance" } }, + "videoPlaybackError": "No Internet connection. Please check your connection and ", "serverCoinErrors": { "default": { "DEF_0000": { @@ -1041,7 +1181,8 @@ }, "validation": { "generic": { - "required": "此字段是必需的" + "required": "此字段是必需的", + "englishOnly": "Only ASCII characters and English text are allowed" }, "email": { "invalid": "这不是一个有效的电子邮件" @@ -1452,6 +1593,338 @@ "info": "这是一个 ${derivationSchemeName} xpub。将其导入第三方钱包时,请选择相同的派生模式", "advanced": "高级" } + }, + "inheritanceSyncPlans": { + "enterEmail": { + "title": "Please enter your primary email associated with this plan", + "subTitle": "An OTP will be sent to this email", + "button": "Verify" + }, + "verifyEmail": { + "title": "OTP verification" + } + }, + "inheritancePlanLogin": { + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + } + }, + "fetchData": { + "title": "Please wait while we fetch the plan data", + "subTitle": "Do not disconnect or close the app" + }, + "verifyEmail": { + "title": "OTP Verification" + } + }, + "inheritanceEditExecutorMessage": { + "fetchData": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "editMessage": { + "title": "Executor Message", + "form": { + "messageField": { + "label": "Message", + "placeholder": "Executor Message" + } + }, + "buttons": { + "exit": "Exit without saving", + "save": "Save changes" + }, + "loading": { + "title": "Please wait while we update the data", + "subtitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Executor message successfully encrypted" + } + }, + "inheritanceEditReminderTime": { + "fetchData": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "reminderSetup": { + "title": "Reminder setup", + "reminderInfo": { + "subtitle": "What is reminder time?", + "subtext": "Reminder Time is the interval at which you'll receive a confirmation email from Cypherock to verify your continued well-being. If no response is received within two months of the initial email, the inheritance process will commence and your nominee will receive the recovery instruction email from Cypherock" + }, + "form": { + "reminderField": { + "label": "Set reminder time interval", + "month": "Month", + "months": "Months" + } + }, + "currentReminder": "The current reminder is set at ${month}", + "loading": { + "title": "Please wait while we update the data", + "subtitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Reminders duration updated successfully" + } + }, + "inheritanceEditUserDetails": { + "editDetails": { + "title": "Edit ${userType} Details", + "userTypes": { + "owner": "Owner", + "nominee": "Nominee", + "executor": "Executor" + }, + "buttons": { + "verifyEmail": "Verify Email" + } + }, + "verifyOtp": { + "loading": { + "title": "Please wait while we update the data", + "subtext": "Do not disconnect or close the app" + } + }, + "success": { + "title": "${userType} details successfully updated" + } + }, + "inheritancePinRecovery": { + "title": "PIN Recovery", + "sync": { + "name": "Syncing", + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirmAuth": "Confirm wallet authentication on device", + "tapCard": "Tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 2 beep sounds" + } + }, + "verifyOtp": { + "title": "OTP verification" + }, + "fetch": { + "title": "Please wait while we fetch the encrypted PIN", + "subTitle": "Do no disconnect or close the app" + } + }, + "decryptPin": { + "name": "Decryption", + "title": "Follow instructions on the X1 Vault", + "actions": { + "confirmOnDevice": "Confirm decryption on device", + "tapCard": "Tap any one card" + }, + "messageBox": { + "warning": "The PIN will be decrypted by the card" + }, + "error": { + "title": "PIN Decryption Failed", + "subTitle": "PIN decryption was unsuccessful. Wallet ${wallet} was not found in the card" + } + }, + "viewPin": { + "name": "View PIN", + "title": "Follow instructions on the X1 Vault", + "subTitle": "PIN for wallet ", + "actions": { + "viewDevice": "View the PIN on your device and confirm" + } + }, + "success": { + "name": "Confirmation", + "title": "Congratulations, Pin recovery is complete" + } + }, + "inheritanceEditEncryptedMessage": { + "confirmation": { + "title": "Are you sure you want to edit the encrypted nominee message?", + "subTitle": "You will need the device and at least one card to encrypt the message again after editing" + }, + "syncing": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + }, + "decryption": { + "wallet": { + "title": "Follow instructions on the X1 Vault", + "actions": { + "confirm": "Confirm decryption on device", + "tapCard": "Tap any card" + }, + "messageBox": { + "warning": "The message will be decrypted by the X1 card" + }, + "decryption": { + "title": "Please wait while we decrypt the data", + "subTitle": "Do not disconnect or close the app" + } + } + }, + "editMessage": { + "title": "Nominee message", + "subTitle": "The nominee messages will be end-to-end encrypted", + "form": { + "cardLocationField": { + "label": "Card location & Passphrase", + "placeholder": "Write the location of the card", + "tooltip": "Write down the location of another card for the nominee to fetch. Nominee will only be able to recover crypto assets after getting another card. Remember this card should be different than the card(s) you may have given to your nominee(s).\nYou can also include the passphrase in this message" + }, + "personalMessageField": { + "label": "Personal message", + "placeholder": "Write your message", + "tooltip": "This can include your will/message for the nominee and other important pieces of information that you may want your nominee to know such as your crypto exchange account details, coins you have amongst others." + } + }, + "messageBox": { + "warning": "Make sure you describe the card's location clearly for the nominee" + } + }, + "confirmMessage": { + "title": "Follow instructions on the X1 Vault to verify the nominee message", + "subTitle": "You can encrypt the message in the next step", + "actions": { + "confirmOnDevice": "Confirm on device", + "verifyLocation": "Verify Card location in your device" + }, + "messageBox": { + "danger": "Read through the whole text and verify that the message shown in the device is same as you entered in cySync" + } + }, + "encryption": { + "title": "Follow instructions on the X1 Vault to encrypt the nominee message", + "subTitle": "Lift your card after 2 beep sounds", + "tooltip": "", + "actions": { + "enterPinAndTap": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The nominee message will be encrypted by the Card" + }, + "syncing": { + "title": "Please wait while we save the encrypted data on the server", + "subTitle": "Do not disconnect or close the app" + } + }, + "success": { + "title": "Nominee message successfully updated" + } + }, + "inheritanceEstateRecovery": { + "title": "Estate recovery", + "instructions": { + "name": "Clear Device Data", + "dialogs": { + "settings": { + "title": "From the Main Menu of your X1 Vault, click \"Settings\"" + }, + "clearData": { + "title": "Click \"Clear Device Data\" on your X1 Vault" + }, + "confirmClearData": { + "title": "Confirm \"Clear device data\" on the X1 Vault" + }, + "tapCards": { + "title": "Tap any 2 old X1 Cards below the X1 Vault", + "subTitle": "Do not lift until you hear a beep sound", + "messageBoxList": [ + { + "warning": "Tap the cards you used during the initial Cypherock setup process, not the cards given by the wallet owner" + }, + { + "warning": "Your device will restart after this processs" + } + ] + } + } + }, + "wallet": { + "name": "Wallet", + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Tap the card you received as a nominee" + }, + "messageBox": { + "warning": "Do not lift until you hear 2 beep sound" + }, + "verification": { + "title": "OTP verification" + }, + "syncing": { + "title": "Please wait while we fetch the data", + "subTitle": "Do not disconnect or close the app" + } + }, + "decryption": { + "name": "Decryption", + "device": { + "title": "Follow instructions on the X1 Vault", + "subTitle": "Do not lift the card till you hear a beep sound", + "actions": { + "confirm": "Confirm decryption on device", + "tapCard": "Tap the card you received as a nominee" + }, + "messageBox": { + "warning": "The message will be decrypted by the card you received as a nominee" + } + }, + "error": { + "title": "Decrypted message", + "message": "Update was unsuccessful. Wallet ABC was not found in the card" + } + }, + "viewPin": { + "name": "View PIN", + "title": "Follow instructions on the X1 Vault", + "subTitle": "PIN for wallet", + "actions": { + "view": "View & copy the PIN displayed on your device and then confirm" + }, + "messageBox": { + "warning": "You will need this PIN to access crypto assets on this wallet. Make sure you do not forget it." + } + }, + "viewMessage": { + "name": "View Message", + "title": "Message from ${name}", + "tooltip": "These messages were written by the wallet owner for you. They contain important information about how to recover their crypto assets stored on the Cypherock X1. \nNote- Make sure you keep these messages safe and private. ", + "form": { + "cardLocationField": { + "label": "Card location", + "placeholder": "Write the location of the card", + "tooltip": "This is the location of another card written by wallet owner for you to fetch. You will only be able to recover the crypto assets after getting another card" + }, + "personalMessageField": { + "label": "Personal message", + "placeholder": "Write your message", + "tooltip": "This includes a personal message written by wallet owner for you" + }, + "checkBox": { + "label": "I have read and copied the above messages" + } + } + }, + "confirmation": { + "name": "Confirmation", + "success": { + "title": "Congratulations, you just decrypted the message from ", + "subTitle": "Watch the video to understand the next steps" + } + } } }, "toggle": { @@ -1554,5 +2027,555 @@ } } } + }, + "otp": { + "title": "Enter OTP", + "wrongOtpTitle": "Wrong OTP", + "successRedirectTitle": "Please wait while we redirect you...", + "triesRemaining": "${retries} tries remaining", + "infoTexts": [ + "An email has been sent to **${email}**", + "Please enter the code received on the email" + ], + "noRetries": { + "title": "You have 0 tries remaining", + "subTitle": "Please exit the flow and restart" + }, + "buttons": { + "resendWithTimeout": "Resend OTP in ${timeout}s" + } + }, + "inheritance": { + "title": "Cypherock Cover", + "plans": { + "silver": { + "title": "Silver", + "description": "Secure your wallet PIN with us and easily recover it if forgotten, ensuring continuous access to your crypto assets" + }, + "gold": { + "title": "Gold", + "description": "Ensure your crypto assets are inherited by loved ones effortlessly, bypassing complex legal restrictions for smooth transfers" + } + }, + "choosePlan": { + "title": "Choose a plan", + "plans": { + "buttonText": "SELECT", + "popularTagText": "MOST POPULAR", + "features": [ + "No KYC required", + "Multi-Chain", + "PIN Recovery", + "Estate Recovery", + "Passphrase Recovery" + ] + } + }, + "homePage": { + "headers": { + "owner": { + "title": "Owner", + "subtitle": "Setup recovery for your wallets" + }, + "nominee": { + "title": "Nominee", + "subtitle": "You are a nominee of the following wallets" + } + }, + "setup": { + "setupCover": { + "title": "Setup Cypherock Cover", + "subTitle": "Protect your crypto assets and ensure smooth transfer to your beneficiaries in unforeseen circumstances" + }, + "syncFromMail": { + "title": "Sync from Email", + "subTitle": "If you're already part of a Cypherock Cover plan, either as the plan owner or a beneficiary" + }, + "learnMore": { + "title": "What is Cypherock Cover?", + "subTitle": "Learn how Cypherock Cover secures your crypto assets and ensures their seamless transfer to your loved ones" + } + } + }, + "planDetails": { + "walletDetails": { + "createdOn": "Created on", + "expiringOn": "Expiring on", + "reminderPeriodField": { + "label": "Reminder Period", + "input": "Every ${month} month", + "inputPlural": "Every ${month} months" + } + }, + "ownerDetails": { + "title": "Owner Details", + "form": { + "userNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + } + } + }, + "nomineeDetails": { + "title": "Nominee #${number}", + "form": { + "nomineeNameField": { + "label": "Nominee Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "encryptedMessage": { + "label": "Encrypted Message" + } + } + }, + "executorDetails": { + "title": "Executor", + "form": { + "nomineeNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "executorMessage": { + "label": "Executor Message" + }, + "assignTo": { + "label": "Assigned to" + } + } + } + }, + "buttons": { + "syncFromEmail": "Sync from Email", + "recoverPin": "Recover PIN", + "renewPlan": "Renew Plan", + "unlock": "Unlock", + "upgradePlan": "Upgrade Plan" + }, + "termsOfService": { + "title": "Terms of Use", + "termsOfService": "Terms of Service", + "privacyPolicy": "Privacy Policy", + "checkBoxLabel": "I have read and agree to Terms of service and Privacy policy" + }, + "dialog": { + "userDetails": { + "form": { + "name": "Name", + "emailField": { + "label": "Primary Email", + "tooltip": "Enter the email on which you want to receive all reminder emails" + }, + "alternateEmail": "Alternate Email" + }, + "error": { + "sameEmail": "Alternate email cannot be same as primary email" + } + }, + "verifyOTP": { + "primaryEmailOTP": { + "title": "OTP Verification for primary email" + }, + "alternateEmailOTP": { + "title": "OTP Verification for alternate email" + } + }, + "payment": { + "heading": "Buy the plan on the Cypherock website", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase", + "form": { + "promoField": { + "label": "Promo code / coupon", + "placeholder": "Enter code" + } + }, + "couponInput": { + "appliedButtonText": "Coupon applied:", + "applyButtonText": "Apply" + }, + "noOfYear": "Number of years", + "total": "Total", + "year": "year", + "error": { + "errorHeading": "Invalid Coupon Code", + "subtext": "Please try a different coupon code" + } + } + }, + "banner": { + "title": "Secure your crypto legacy with Cypherock Cover - Ensure your assets reach your loved ones", + "buttons": { + "knowMore": "Know More" + } + } + }, + "inheritanceSilverPlanPurchase": { + "title": "Silver Plan Setup", + "instructions": { + "heading": "Instructions", + "ensure": { + "title": "Ensure the following before you continue", + "instructions": [ + "Make sure you have 15-30 minutes to set it up", + "You are present in a safe environment", + "You need to have one device and one card handy to set it up" + ] + }, + "video": { + "title": "Watch the tutorial video before you proceed further" + } + }, + "wallet": { + "heading": "Wallet", + "selectWallet": { + "title": "Select the wallet you want to cover in this plan", + "subTitle": "For setting up additional wallets individual plans can be created later", + "tooltip": "This wallet is not having a PIN setup, create a new wallet or select a different wallet having PIN setup" + }, + "walletAuth": { + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirm": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 3 beep sounds" + } + } + }, + "email": { + "heading": "Owner", + "userDetails": { + "title": "Your Details", + "subTitle": "All the fields are mandatory", + "buttons": { + "sendOTP": "Send OTP" + } + } + }, + "encryption": { + "heading": "Encryption", + "device": { + "title": "Follow instructions on the X1 Vault to encrypt the PIN", + "subTitle": "Lift your card after 2 beep sounds", + "tooltip": "Tap any one X1 card below the X1 device to encrypt the message and do not remove the card until you hear 2 beep sounds", + "actions": { + "confirm": "Confirm encryption on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The PIN will be encrypted by the X1 card" + } + }, + "loading": { + "title": "Please wait while we save the encrypted data on the server", + "subTitle": "Do not disconnect or close the app" + }, + "success": { + "title": "PIN is successfully encrypted and saved", + "subTitle": "To complete your silver plan setup, you can now proceed with the plan purchase" + } + }, + "checkout": { + "heading": "Checkout", + "payment": { + "title": "Complete your payment", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase" + }, + "success": { + "title": "Thank you!", + "subtext": "Your Silver Plan is now activated" + } + } + }, + "inheritanceGoldPlanPurchase": { + "title": "Gold Plan Setup", + "instructions": { + "heading": "Instructions", + "ensure": { + "title": "Ensure the following before you continue", + "instructions": [ + "Make sure you have 15-30 minutes to set it up", + "You are present in a safe environment", + "You need to have one device and one card handy to set it up" + ] + }, + "video": { + "title": "Watch the tutorial video before you proceed", + "subTitle": "The video will help you take the right decisions" + } + }, + "wallet": { + "heading": "Wallet", + "selectWallet": { + "title": "Select the wallet you want to cover in this plan", + "subTitle": "For setting up additional wallets, individual plans can be created later", + "tooltip": "This wallet is not having a PIN setup, create a new wallet or select a different wallet having PIN setup" + }, + "walletAuth": { + "heading": "Wallet Authentication", + "title": "Follow instructions on the X1 Vault to authenticate the selected wallet", + "subTitle": "Authenticating the wallet ", + "actions": { + "confirmOnDevice": "Confirm wallet authentication on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "Lift your card after 3 beep sounds" + }, + "error": { + "title": "Wallet Authentication failed", + "subtext": "We could not authenticate the selected wallet", + "messageBox": { + "warning": "This usually happens when the wallets in the Cysync and the device are not in sync. Sync the wallets between the Cysync and the device or choose a different wallet" + } + } + } + }, + "email": { + "heading": "Owner", + "userDetails": { + "title": "Your Details", + "form": { + "name": "Name", + "emailField": { + "label": "Primary Email", + "tooltip": "Enter the email on which you want to receive all reminder emails" + }, + "alternateEmail": "Alternate Email" + }, + "buttons": { + "sendOTP": "Send OTP" + } + }, + "primaryEmailOTP": { + "title": "OTP Verification for primary email" + }, + "alternateEmailOTP": { + "title": "OTP Verification for alternate email" + } + }, + "nomineeAndExecutor": { + "heading": "Nominees & Executor", + "select": { + "title": "How many nominees do you want?", + "subtitle": "You can have up to 2 nominees for this will", + "tooltip": "In case if you select two nominees then the 2nd nominee will receive the recovery email three months after the 1st nominee receives it\nNote - Both nominees will receive the same recovery email and same message written by you on the CySync app", + "options": { + "descOne": "Select if you want to have only one nominee", + "descTwo": "Select if you want to have two nominees" + } + }, + "nomineeDetails": { + "first": { + "title": "Add first nominee", + "warning": "Make sure you give a card to your nominee", + "tooltip": "Your Nominee will receive recovery instructions on this email" + }, + "second": { + "title": "Add second nominee", + "warning": "Make sure you give a card to your second nominee" + }, + "confirm": { + "title": "Do you want to verify nominee emails?", + "subtext": "An OTP will be sent to the provided emails. If you do not want to inform the nominee about this plan, you can skip verification of emails.", + "warning": "If you do not verify the nominee email then Cypherock emails may land in spam folder" + } + }, + "executor": { + "select": { + "title": "Do you want to have an executor for this will?", + "subtext": "The executor will help the nominee to recover funds ", + "tooltip": "An executor/helper can be someone you trust who does not have any financial stake but can be assigned as a helper to the nominee for funds recovery\nThe executor will receive the email on the same time as the nominee.", + "options": { + "one": { + "yes": "Yes", + "desc": "A tech savvy person whom you trust" + }, + "two": { + "no": "No", + "desc": "If your nominees are tech savvy" + } + } + }, + "executorDetails": { + "title": "Add executor", + "tooltip": "Executor will receive instructions to help nominee on this email", + "radio": { + "label": "Choose a nominee to link with executor", + "tooltip": "Select the nominee you want to link with the executor. If you choose Nominee 1 then the executor will receive their email written by you on same time as Nominee 1.\n\nIf you choose Nominee 2 then the executor will receive their email written by you on same time as Nominee 2 i.e, 3 months after Nominee 1.", + "options": { + "labelPrefix": "Nominee " + } + } + } + } + }, + "message": { + "heading": "Message", + "tutorial": { + "title": "Watch the tutorial video before you proceed further", + "subtext": "How to write the final messages" + }, + "nominee": { + "title": "Draft the final message for the nominee", + "subtitle": "The nominee messages will be end-to-end encrypted", + "tooltip": "End-to-end encryption keeps your messages between you and the nominee you choose. Not even Cypherock can read them", + "form": { + "locationField": { + "label": "Card Location & Passphrase", + "placeholder": "Write the location of the card", + "tooltip": "Write down the location of another card for the nominee to fetch. Nominee will only be able to recover crypto assets after getting another card. Remember this card should be different than the card(s) you may have given to your nominee(s).\nYou can also include the passphrase in this message." + }, + "personalMessage": { + "label": "Personal Message", + "placeholder": "Write your message", + "tooltip": "This can include your will/message for the nominee and other important pieces of information that you may want your nominee to know such as your crypto exchange account details, coins you have amongst others." + } + }, + "messageBox": { + "warning": "Make sure you describe the card's location clearly for the nominee" + } + }, + "executor": { + "title": "Add a message for the executor", + "subtitle": "Please note that the message to the executor is not encrypted", + "tooltip": "This message will be received by the executor in case recovery is triggered, it's recommended to NOT add any critical information here", + "form": { + "messageField": { + "label": "Message", + "placeholder": "Write your message" + } + } + } + }, + "reminder": { + "heading": "Reminder", + "title": "Reminder setup" + }, + "summary": { + "heading": "Summary", + "title": "Summary", + "subtitle": "Please verify all the details before it gets encrypted with the device", + "ownerDetails": { + "title": "Owner Details", + "form": { + "userNameField": { + "label": "Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "reminderPeriodField": { + "label": "Reminder Period", + "input": "Every ${month} month", + "inputPlural": "Every ${month} months" + } + } + }, + "nomineeDetails": { + "title": "Nominee #", + "form": { + "nomineeNameField": { + "label": "Nominee Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + } + } + }, + "cardLocation": { + "title": "Card Location & Passphrase" + }, + "personalMessage": { + "title": "Personal Message" + }, + "executorDetails": { + "title": "Executor", + "form": { + "nomineeNameField": { + "label": "Nominee Name" + }, + "primaryEmailField": { + "label": "Primary Email" + }, + "secondaryEmailField": { + "label": "Secondary Email" + }, + "assignTo": { + "label": "Assigned to" + } + } + }, + "executorMessage": { + "title": "Executor Message" + } + }, + "encryption": { + "heading": "Encryption", + "confirm": { + "title": "Follow instructions on the X1 Vault to verify the nominee message", + "subtext": "You can encrypt the message in the next step", + "actions": { + "confirmOnDevice": "Confirm message verification on device", + "verifyLocation": "Verify Card Location in your device" + }, + "messageBox": { + "danger": "Read through the whole text and verify that the message shown in the device is same as you entered in cySync" + } + }, + "device": { + "title": "Follow instructions on the X1 Vault to encrypt the wallet PIN", + "subtext": "Lift your card after 2 beep sounds ", + "tooltip": "Tap any one X1 card below the X1 device to encrypt the message and do not remove the card until you hear 2 beep sounds", + "actions": { + "confirmOnDevice": "Confirm encryption on device", + "tapCard": "Enter PIN and tap any one card" + }, + "messageBox": { + "warning": "The PIN will be encrypted by the X1 card" + } + }, + "loading": { + "title": "Please wait while we send the encrypted data to the server", + "subTitle": "Do not disconnect or close the app" + }, + "success": { + "title": "Nominee message successfully encrypted", + "subtext": "To complete your gold plan setup, you can now proceed with the plan purchase" + } + }, + "checkout": { + "heading": "Checkout", + "payment": { + "title": "Complete your payment", + "subtext": "You have to purchase the plan on the Cypherock website and then apply the generated coupon code in the 'Coupon Code' section below to complete your purchase" + }, + "greeting": { + "title": "Thank you!", + "subtext": "Your Gold Plan is now activated" + } + } } } diff --git a/packages/cysync-core-constants/src/i18n/types.ts b/packages/cysync-core-constants/src/i18n/types.ts index fd7a37015..eb148713c 100644 --- a/packages/cysync-core-constants/src/i18n/types.ts +++ b/packages/cysync-core-constants/src/i18n/types.ts @@ -11,7 +11,9 @@ export interface LanguageStrings { help: string; back: string; allWallets: string; + labels: LangLabels; buttons: LangButtons; + dashboard: LangDashboard; tooltips: LangTooltips; deviceAuthentication: LangDeviceAuthentication; lockscreen: LangLockscreen; @@ -41,6 +43,15 @@ export interface LanguageStrings { toggle: LangToggle; snackbar: LangSnackbar; settings: LangSettings; + otp: LangOTP; + inheritance: LangInheritance; + inheritanceSilverPlanPurchase: LangInheritanceSilverPlanPurchase; + inheritanceGoldPlanPurchase: LangInheritanceGoldPlanPurchase; +} + +interface LangLabels { + email: string; + required: string; } interface LangButtons { @@ -81,6 +92,37 @@ interface LangButtons { editAccount: string; submit: string; showMore: string; + resendOTP: string; + next: string; + setup: string; + sync: string; + learnMore: string; + checkout: string; + yes: string; + no: string; + saveAndContinue: string; + exitWithoutSaving: string; + saveChanges: string; + edit: string; + tryAgain: string; +} + +interface LangDashboard { + wallet: { + renewNow: string; + buyNow: string; + created: string; + expiredOn: string; + expiresIn: string; + expiry: string; + expiring: string; + expired: string; + pending: string; + silver: string; + gold: string; + hours: string; + setupCover: string; + }; } interface LangTooltips { @@ -485,9 +527,11 @@ interface LangSidebar { receiveCrypto: string; history: string; walletConnect: string; + cypherockCover: string; tutorial: string; settings: string; help: string; + new: string; tooltip: { walletDeleted: string }; } @@ -593,10 +637,14 @@ export interface LangErrors { Record> >; default: string; + videoPlaybackError: string; } interface LangValidation { - generic: { required: string }; + generic: { + required: string; + englishOnly: string; + }; email: { invalid: string }; password: { mismatch: string; @@ -746,6 +794,318 @@ interface LangDialogs { advanced: string; }; }; + inheritanceSyncPlans: { + enterEmail: { + title: string; + subTitle: string; + button: string; + }; + verifyEmail: { + title: string; + }; + }; + inheritancePlanLogin: { + walletAuth: { + title: string; + subTitle: string; + actions: { + confirm: string; + tapCard: string; + }; + }; + fetchData: { + title: string; + subTitle: string; + }; + verifyEmail: { + title: string; + }; + }; + inheritanceEditExecutorMessage: { + fetchData: { + title: string; + subTitle: string; + }; + editMessage: { + title: string; + form: { + messageField: { label: string; placeholder: string }; + }; + buttons: { + exit: string; + save: string; + }; + loading: { + title: string; + subtitle: string; + }; + }; + success: { + title: string; + }; + }; + inheritanceEditReminderTime: { + fetchData: { + title: string; + subTitle: string; + }; + reminderSetup: { + title: string; + reminderInfo: { + subtitle: string; + subtext: string; + }; + form: { + reminderField: { label: string; month: string; months: string }; + }; + currentReminder: string; + loading: { + title: string; + subtitle: string; + }; + }; + success: { + title: string; + }; + }; + inheritanceEditUserDetails: { + editDetails: { + title: string; + userTypes: { + owner: string; + nominee: string; + executor: string; + }; + buttons: { + verifyEmail: string; + }; + }; + verifyOtp: { + loading: { + title: string; + subtext: string; + }; + }; + success: { + title: string; + }; + }; + inheritancePinRecovery: { + title: string; + sync: { + name: string; + walletAuth: { + title: string; + subTitle: string; + actions: { + confirmAuth: string; + tapCard: string; + }; + messageBox: { + warning: string; + }; + }; + verifyOtp: { + title: string; + }; + fetch: { + title: string; + subTitle: string; + }; + }; + decryptPin: { + name: string; + title: string; + actions: { + confirmOnDevice: string; + tapCard: string; + }; + messageBox: { + warning: string; + }; + error: { + title: string; + subTitle: string; + }; + }; + viewPin: { + name: string; + title: string; + subTitle: string; + actions: { + viewDevice: string; + }; + }; + success: { + name: string; + title: string; + }; + }; + inheritanceEditEncryptedMessage: { + confirmation: { + title: string; + subTitle: string; + }; + syncing: { + title: string; + subTitle: string; + }; + decryption: { + wallet: { + title: string; + actions: { + confirm: string; + tapCard: string; + }; + messageBox: { + warning: string; + }; + decryption: { + title: string; + subTitle: string; + }; + }; + }; + editMessage: { + title: string; + subTitle: string; + form: { + cardLocationField: { + label: string; + placeholder: string; + tooltip: string; + }; + personalMessageField: { + label: string; + placeholder: string; + tooltip: string; + }; + }; + messageBox: { + warning: string; + }; + }; + confirmMessage: { + title: string; + subTitle: string; + actions: { + confirmOnDevice: string; + verifyLocation: string; + }; + messageBox: { + danger: string; + }; + }; + encryption: { + title: string; + subTitle: string; + tooltip: string; + actions: { + enterPinAndTap: string; + }; + messageBox: { + warning: string; + }; + syncing: { + title: string; + subTitle: string; + }; + }; + success: { + title: string; + }; + }; + inheritanceEstateRecovery: { + title: string; + instructions: { + name: string; + dialogs: { + settings: { title: string }; + clearData: { title: string }; + confirmClearData: { title: string }; + tapCards: { + title: string; + subTitle: string; + messageBoxList: { warning: string }[]; + }; + }; + }; + wallet: { + name: string; + title: string; + subTitle: string; + actions: { + confirm: string; + tapCard: string; + }; + messageBox: { + warning: string; + }; + verification: { + title: string; + }; + syncing: { + title: string; + subTitle: string; + }; + }; + decryption: { + name: string; + device: { + title: string; + subTitle: string; + actions: { + confirm: string; + tapCard: string; + }; + messageBox: { + warning: string; + }; + }; + error: { + title: string; + message: string; + }; + }; + viewPin: { + name: string; + title: string; + subTitle: string; + actions: { + view: string; + }; + messageBox: { + warning: string; + }; + }; + viewMessage: { + name: string; + title: string; + tooltip: string; + form: { + cardLocationField: { + label: string; + placeholder: string; + tooltip: string; + }; + personalMessageField: { + label: string; + placeholder: string; + tooltip: string; + }; + checkBox: { + label: string; + }; + }; + }; + confirmation: { + name: string; + success: { + title: string; + subTitle: string; + }; + }; + }; } interface LangToggle { @@ -800,3 +1160,491 @@ interface LangSettings { }; }; } + +interface LangOTP { + title: string; + wrongOtpTitle: string; + successRedirectTitle: string; + triesRemaining: string; + infoTexts: string[]; + noRetries: { + title: string; + subTitle: string; + }; + buttons: { + resendWithTimeout: string; + }; +} + +interface LangInheritance { + title: string; + plans: { + silver: { + title: string; + description: string; + }; + gold: { + title: string; + description: string; + }; + }; + choosePlan: { + title: string; + plans: { + buttonText: string; + popularTagText: string; + features: string[]; + }; + }; + homePage: { + headers: { + owner: { + title: string; + subtitle: string; + }; + nominee: { + title: string; + subtitle: string; + }; + }; + setup: { + setupCover: { + title: string; + subTitle: string; + }; + syncFromMail: { + title: string; + subTitle: string; + }; + learnMore: { + title: string; + subTitle: string; + }; + }; + }; + planDetails: { + walletDetails: { + createdOn: string; + expiringOn: string; + reminderPeriodField: { + label: string; + input: string; + inputPlural: string; + }; + }; + ownerDetails: { + title: string; + form: { + userNameField: { label: string }; + primaryEmailField: { label: string }; + secondaryEmailField: { label: string }; + }; + }; + nomineeDetails: { + title: string; + form: { + nomineeNameField: { label: string }; + primaryEmailField: { label: string }; + secondaryEmailField: { label: string }; + encryptedMessage: { label: string }; + }; + }; + executorDetails: { + title: string; + form: { + nomineeNameField: { label: string }; + primaryEmailField: { label: string }; + secondaryEmailField: { label: string }; + executorMessage: { label: string }; + assignTo: { label: string }; + }; + }; + }; + buttons: { + syncFromEmail: string; + recoverPin: string; + renewPlan: string; + unlock: string; + upgradePlan: string; + }; + termsOfService: { + title: string; + termsOfService: string; + privacyPolicy: string; + checkBoxLabel: string; + }; + dialog: { + userDetails: { + form: { + name: string; + emailField: { + label: string; + tooltip: string; + }; + alternateEmail: string; + }; + error: { + sameEmail: string; + }; + }; + verifyOTP: { + primaryEmailOTP: { + title: string; + }; + alternateEmailOTP: { + title: string; + }; + }; + payment: { + heading: string; + form: { + promoField: { label: string; placeholder: string }; + }; + noOfYear: string; + total: string; + year: string; + couponInput: { + applyButtonText: string; + appliedButtonText: string; + }; + error: { + errorHeading: string; + subtext: string; + }; + }; + }; + banner: { + title: string; + buttons: { + knowMore: string; + }; + }; +} + +interface LangInheritanceSilverPlanPurchase { + title: string; + instructions: { + heading: string; + ensure: { + title: string; + instructions: string[]; + }; + video: { + title: string; + }; + }; + wallet: { + heading: string; + selectWallet: { + title: string; + subTitle: string; + tooltip: string; + }; + walletAuth: { + title: string; + subTitle: string; + actions: { + confirm: string; + tapCard: string; + }; + messageBox: { + warning: string; + }; + }; + }; + email: { + heading: string; + userDetails: { + title: string; + subTitle: string; + buttons: { + sendOTP: string; + }; + }; + }; + encryption: { + heading: string; + device: { + title: string; + subTitle: string; + tooltip: string; + actions: { + confirm: string; + tapCard: string; + }; + messageBox: { + warning: string; + }; + }; + loading: { + title: string; + subTitle: string; + }; + success: { + title: string; + subTitle: string; + }; + }; + checkout: { + heading: string; + payment: { + title: string; + subtext: string; + }; + success: { + title: string; + subtext: string; + }; + }; +} + +interface LangInheritanceGoldPlanPurchase { + title: string; + instructions: { + heading: string; + ensure: { + title: string; + instructions: string[]; + }; + video: { + title: string; + }; + }; + wallet: { + heading: string; + selectWallet: { + title: string; + subTitle: string; + tooltip: string; + }; + walletAuth: { + heading: string; + title: string; + subTitle: string; + actions: { + confirmOnDevice: string; + tapCard: string; + }; + messageBox: { + warning: string; + }; + error: { + title: string; + subtext: string; + messageBox: { + warning: string; + }; + }; + }; + }; + email: { + heading: string; + userDetails: { + title: string; + form: { + name: string; + emailField: { + label: string; + tooltip: string; + }; + alternateEmail: string; + }; + buttons: { + sendOTP: string; + }; + }; + primaryEmailOTP: { + title: string; + }; + alternateEmailOTP: { + title: string; + }; + }; + nomineeAndExecutor: { + heading: string; + select: { + title: string; + subtitle: string; + tooltip: string; + options: { + descOne: string; + descTwo: string; + }; + }; + nomineeDetails: { + first: { + title: string; + warning: string; + tooltip: string; + }; + second: { + title: string; + warning: string; + }; + confirm: { + title: string; + subtext: string; + warning: string; + }; + }; + executor: { + select: { + title: string; + subtext: string; + tooltip: string; + options: { + one: { + yes: string; + desc: string; + }; + two: { + no: string; + desc: string; + }; + }; + }; + executorDetails: { + title: string; + tooltip: string; + radio: { + label: string; + tooltip: string; + options: { + labelPrefix: string; + }; + }; + }; + }; + }; + message: { + heading: string; + tutorial: { + title: string; + subtext: string; + }; + nominee: { + title: string; + subtitle: string; + tooltip: string; + form: { + locationField: { + label: string; + placeholder: string; + tooltip: string; + }; + personalMessage: { + label: string; + placeholder: string; + tooltip: string; + }; + }; + messageBox: { + warning: string; + }; + }; + executor: { + title: string; + subtitle: string; + tooltip: string; + form: { + messageField: { + label: string; + placeholder: string; + }; + }; + }; + }; + reminder: { + heading: string; + title: string; + }; + summary: { + heading: string; + title: string; + subtitle: string; + ownerDetails: { + title: string; + form: { + userNameField: { label: string }; + primaryEmailField: { label: string }; + secondaryEmailField: { label: string }; + reminderPeriodField: { + label: string; + input: string; + inputPlural: string; + }; + }; + }; + nomineeDetails: { + title: string; + form: { + nomineeNameField: { label: string }; + primaryEmailField: { label: string }; + secondaryEmailField: { label: string }; + }; + }; + cardLocation: { + title: string; + }; + personalMessage: { + title: string; + }; + executorDetails: { + title: string; + form: { + nomineeNameField: { label: string }; + primaryEmailField: { label: string }; + secondaryEmailField: { label: string }; + assignTo: { label: string }; + }; + }; + executorMessage: { + title: string; + }; + }; + encryption: { + heading: string; + confirm: { + title: string; + subtext: string; + actions: { + confirmOnDevice: string; + verifyLocation: string; + }; + messageBox: { + danger: string; + }; + }; + device: { + title: string; + subtext: string; + tooltip: string; + actions: { + confirmOnDevice: string; + tapCard: string; + }; + messageBox: { + warning: string; + }; + }; + loading: { + title: string; + subTitle: string; + }; + success: { + title: string; + subtext: string; + }; + }; + checkout: { + heading: string; + payment: { + title: string; + subtext: string; + }; + greeting: { + title: string; + subtext: string; + }; + }; +} diff --git a/packages/cysync-core-constants/src/types/serverError.ts b/packages/cysync-core-constants/src/types/serverError.ts index 9c8885ed1..5c4fab300 100644 --- a/packages/cysync-core-constants/src/types/serverError.ts +++ b/packages/cysync-core-constants/src/types/serverError.ts @@ -1,6 +1,32 @@ export enum ServerErrorType { UNKNOWN_ERROR = 'SER_0000', CONNOT_CONNECT = 'SER_0001', + + OTP_VERIFICATION_FAILED = 'SER_1001', + LOGIN_FAILED = 'SER_1003', + SIGNATURE_VERIFICATION_FAILED = 'SER_1004', + INVALID_REQUEST = 'SER_1005', + UNAUTHORIZED_ACCESS = 'SER_1006', + RESOURCE_NOT_FOUND = 'SER_1007', + INTERNAL_SERVER_ERROR = 'SER_1008', + REQUEST_TIMEOUT = 'SER_1009', + OTP_EXPIRED = 'SER_1010', + PAYLOAD_VALIDATION_ERROR = 'SER_1011', + MAX_RETRIES_EXCEEDED = 'SER_1012', + ACCOUNT_LOCKED = 'SER_1013', + SERVICE_UNAVAILABLE = 'SER_1014', + REQUEST_CONFLICT = 'SER_1015', + VALIDATION_ERROR = 'SER_1016', + MISSING_WALLET = 'SER_1017', + MISSING_USER = 'SER_1018', + MISSING_NOMINEE = 'SER_1019', + MISSING_EXECUTOR = 'SER_1020', + MISSING_SESSION = 'SER_1021', + INVALID_COUPON = 'SER_1022', + INVALID_SESSION = 'SER_1023', + INVALID_DEVICE = 'SER_1024', + INVALID_PRIVATE_KEY = 'SER_1025', + ACTIVE_PLAN_FOUND = 'SER_1026', } type CodeToErrorMap = { @@ -16,6 +42,81 @@ export const serverErrorTypeDetails: CodeToErrorMap = { [ServerErrorType.CONNOT_CONNECT]: { message: 'Cannot connect to the server', }, + [ServerErrorType.OTP_VERIFICATION_FAILED]: { + message: 'OTP verification failed', + }, + [ServerErrorType.LOGIN_FAILED]: { + message: 'Login failed', + }, + [ServerErrorType.SIGNATURE_VERIFICATION_FAILED]: { + message: 'Signature verification failed', + }, + [ServerErrorType.INVALID_REQUEST]: { + message: 'Invalid request', + }, + [ServerErrorType.UNAUTHORIZED_ACCESS]: { + message: 'Unauthorized access', + }, + [ServerErrorType.RESOURCE_NOT_FOUND]: { + message: 'Resource not found', + }, + [ServerErrorType.INTERNAL_SERVER_ERROR]: { + message: 'Internal server error', + }, + [ServerErrorType.REQUEST_TIMEOUT]: { + message: 'Request timeout', + }, + [ServerErrorType.OTP_EXPIRED]: { + message: 'OTP expired', + }, + [ServerErrorType.PAYLOAD_VALIDATION_ERROR]: { + message: 'Payload validation error', + }, + [ServerErrorType.MAX_RETRIES_EXCEEDED]: { + message: 'Maximum retries exceeded', + }, + [ServerErrorType.ACCOUNT_LOCKED]: { + message: 'Account locked', + }, + [ServerErrorType.SERVICE_UNAVAILABLE]: { + message: 'Service unavailable', + }, + [ServerErrorType.REQUEST_CONFLICT]: { + message: 'Request Conflict', + }, + [ServerErrorType.VALIDATION_ERROR]: { + message: 'VALIDATION_ERROR', + }, + [ServerErrorType.MISSING_WALLET]: { + message: 'MISSING_WALLET', + }, + [ServerErrorType.MISSING_USER]: { + message: 'MISSING_USER', + }, + [ServerErrorType.MISSING_NOMINEE]: { + message: 'MISSING_NOMINEE', + }, + [ServerErrorType.MISSING_EXECUTOR]: { + message: 'MISSING_EXECUTOR', + }, + [ServerErrorType.MISSING_SESSION]: { + message: 'MISSING_SESSION', + }, + [ServerErrorType.INVALID_COUPON]: { + message: 'INVALID_COUPON', + }, + [ServerErrorType.INVALID_SESSION]: { + message: 'INVALID_SESSION', + }, + [ServerErrorType.INVALID_DEVICE]: { + message: 'INVALID_DEVICE', + }, + [ServerErrorType.INVALID_PRIVATE_KEY]: { + message: 'INVALID_PRIVATE_KEY', + }, + [ServerErrorType.ACTIVE_PLAN_FOUND]: { + message: 'ACTIVE_PLAN_FOUND', + }, }; export interface ServerErrorDetails { diff --git a/packages/cysync-core/package.json b/packages/cysync-core/package.json index 0a52b5a63..dbafa01cd 100644 --- a/packages/cysync-core/package.json +++ b/packages/cysync-core/package.json @@ -65,6 +65,7 @@ }, "dependencies": { "@aws-crypto/sha256-browser": "^4.0.0", + "@cypherock/app-support-inheritance": "workspace:^", "@cypherock/coin-support": "workspace:^", "@cypherock/coin-support-btc": "workspace:^", "@cypherock/coin-support-evm": "workspace:^", diff --git a/packages/cysync-core/src/actions/dialog/index.ts b/packages/cysync-core/src/actions/dialog/index.ts index de730d0f1..e3e6f5959 100644 --- a/packages/cysync-core/src/actions/dialog/index.ts +++ b/packages/cysync-core/src/actions/dialog/index.ts @@ -6,6 +6,14 @@ import { DeviceAuthenticationDialogProps } from '~/dialogs/DeviceAuthenticationD import { EditAccountDialogProps } from '~/dialogs/EditAccountDialog'; import { ErrorDialogProps } from '~/dialogs/ErrorDialog'; import { IHistoryDialogProps } from '~/dialogs/HistoryDialog'; +import { + InheritanceEditUserDetailsDialogProps, + InheritanceEstateRecoveryDialogProps, + InheritancePinRecoveryDialogProps, + InheritancePlanLoginDialogProps, +} from '~/dialogs/Inheritance'; +import { InheritanceEditExecutorMessageDialogProps } from '~/dialogs/Inheritance/EditExecutorMessage/context'; +import { InheritanceEditReminderTimeDialogProps } from '~/dialogs/Inheritance/EditReminderTime/context'; import { ReceiveDialogProps } from '~/dialogs/Receive'; import { SendDialogProps } from '~/dialogs/Send/'; import { GuidedFlowType, openDialog } from '~/store'; @@ -85,3 +93,39 @@ export const openBetaNotificationDialog = () => export const openEditAccountDialog = (props?: EditAccountDialogProps) => openDialog({ name: 'editAccount', data: props }); + +export const openInheritanceSyncPlansDialog = () => + openDialog({ name: 'inheritanceSyncPlans', data: undefined }); + +export const openInheritancePlanLoginDialog = ( + props: InheritancePlanLoginDialogProps, +) => openDialog({ name: 'inheritancePlanLogin', data: props }); + +export const openInheritanceSilverPlanPurchaseDialog = () => + openDialog({ name: 'inheritanceSilverPlanPurchase', data: undefined }); + +export const openInheritanceGoldPlanPurchaseDialog = () => + openDialog({ name: 'inheritanceGoldPlanPurchase', data: undefined }); + +export const openInheritanceEditExecutorMessageDialog = ( + props?: InheritanceEditExecutorMessageDialogProps, +) => openDialog({ name: 'inheritanceEditExecutorMessage', data: props }); + +export const openInheritanceEditReminderTimeDialog = ( + props: InheritanceEditReminderTimeDialogProps, +) => openDialog({ name: 'inheritanceEditReminderTime', data: props }); + +export const openInheritanceEditUserDetailsDialog = ( + props?: InheritanceEditUserDetailsDialogProps, +) => openDialog({ name: 'inheritanceEditUserDetails', data: props }); + +export const openInheritancePinRecoveryDialog = ( + props: InheritancePinRecoveryDialogProps, +) => openDialog({ name: 'inheritancePinRecovery', data: props }); + +export const openInheritanceEditEncryptedMessageDialog = () => + openDialog({ name: 'inheritanceEditEncryptedMessage', data: undefined }); + +export const openInheritanceEstateRecoveryDialog = ( + props: InheritanceEstateRecoveryDialogProps, +) => openDialog({ name: 'inheritanceEstateRecovery', data: props }); diff --git a/packages/cysync-core/src/bgTask/dbListener/helper.ts b/packages/cysync-core/src/bgTask/dbListener/helper.ts index 35dfd8ba7..83378b302 100644 --- a/packages/cysync-core/src/bgTask/dbListener/helper.ts +++ b/packages/cysync-core/src/bgTask/dbListener/helper.ts @@ -4,6 +4,7 @@ import { syncAccounts } from '~/actions'; import { setAccounts, setDevices, + setInheritancePlans, setLanguage, setPriceHistories, setPriceInfos, @@ -93,6 +94,19 @@ const syncTransactionsDb = createFuncWithErrorHandler( }, ); +const syncInheritancePlanDb = createFuncWithErrorHandler( + 'syncInheritancePlanDb', + async () => { + const db = getDB(); + + // TODO: Unhide pending plans in future when handling is complete + const plans = (await db.inheritancePlan.getAll()).filter( + p => !!(p.expireAt && p.purchasedAt), + ); + store.dispatch(setInheritancePlans(plans)); + }, +); + export const syncAllDb = async (isFirst: boolean) => { await syncAccountsDb(isFirst); await syncWalletsDb(); @@ -100,6 +114,7 @@ export const syncAllDb = async (isFirst: boolean) => { await syncPriceInfosDb(); await syncPriceHistoriesDb(); await syncTransactionsDb(); + await syncInheritancePlanDb(); store.dispatch(setLanguage((await keyValueStore.appLanguage.get()) as any)); }; @@ -119,6 +134,10 @@ export const addListeners = () => { throttleDbFunction(syncPriceHistoriesDb), ); db.transaction.addListener('change', throttleDbFunction(syncTransactionsDb)); + db.inheritancePlan.addListener( + 'change', + throttleDbFunction(syncInheritancePlanDb), + ); }; export const removeListeners = () => { @@ -130,4 +149,5 @@ export const removeListeners = () => { db.priceInfo.removeAllListener(); db.priceHistory.removeAllListener(); db.transaction.removeAllListener(); + db.inheritancePlan.removeAllListener(); }; diff --git a/packages/cysync-core/src/components/FeatureBanner.tsx b/packages/cysync-core/src/components/FeatureBanner.tsx new file mode 100644 index 000000000..c365de97b --- /dev/null +++ b/packages/cysync-core/src/components/FeatureBanner.tsx @@ -0,0 +1,57 @@ +import { + Container, + Typography, + Button, + Image, + cysyncLogoSmall, +} from '@cypherock/cysync-ui'; +import React, { FC, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { routes } from '~/constants'; +import { useNavigateTo } from '~/hooks'; +import { selectLanguage, useAppSelector } from '~/store'; + +export const FeatureBanner: FC = () => { + const lang = useAppSelector(selectLanguage); + const location = useLocation(); + const navigateTo = useNavigateTo(); + + const { path } = routes.portfolio; + + if (!location.pathname.startsWith(path)) { + return null; + } + + const toCypherockCover = useCallback(() => { + navigateTo(routes.inheritance.home.path); + }, [navigateTo]); + + return ( + + + cypherock logo + + {lang.strings.inheritance.banner.title} + + + + + ); +}; diff --git a/packages/cysync-core/src/components/LoaderDialog.tsx b/packages/cysync-core/src/components/LoaderDialog.tsx index ca99ba83d..6ee1c8bfe 100644 --- a/packages/cysync-core/src/components/LoaderDialog.tsx +++ b/packages/cysync-core/src/components/LoaderDialog.tsx @@ -1,14 +1,46 @@ -import { DialogBox, Image, loaderGrayIcon } from '@cypherock/cysync-ui'; +import { + Container, + DialogBox, + DialogBoxBody, + Image, + LangDisplay, + loaderGrayIcon, + Typography, +} from '@cypherock/cysync-ui'; import React from 'react'; -export const LoaderDialog: React.FC = () => ( +export interface LoaderProps { + title?: string; + subtext?: string; +} + +export const LoaderDialog: React.FC = ({ title, subtext }) => ( - Loader icon + + Loader icon + {title && ( + + + + + {subtext && ( + + + + )} + + )} + ); + +LoaderDialog.defaultProps = { + title: undefined, + subtext: undefined, +}; diff --git a/packages/cysync-core/src/components/SideBar.tsx b/packages/cysync-core/src/components/SideBar.tsx index 336dc0ddb..53f63236d 100644 --- a/packages/cysync-core/src/components/SideBar.tsx +++ b/packages/cysync-core/src/components/SideBar.tsx @@ -3,6 +3,8 @@ import { ArrowReceivedIcon, ArrowSentIcon, Button, + Chip, + CypherockCoverIcon, DropDownItemProps, Flex, FloatingMenu, @@ -15,6 +17,7 @@ import { SupportIcon, Synchronizing, TutorialIcon, + Typography, WalletConnectWhiteIcon, WalletIcon, WalletInfoIcon, @@ -165,6 +168,21 @@ const SideBarComponent: FC<{ collapseWallets?: boolean }> = () => { dispatch(openWalletConnectDialog()); }} /> + + + {strings.new} + + + } + onClick={() => navigate('inheritance')} + /> void; +} + +export const VideoPlayer: FC = ({ onRetry, ...props }) => { + const lang = useAppSelector(selectLanguage); + const [error, setError] = useState(false); + const { active } = useAppSelector(selectNetwork); + + useEffect(() => { + if (!active) { + setError(true); + } + }, [active]); + + if (error) { + return ( + + + info icon + + + + + + + ); + } + + return setError(false)} />; +}; diff --git a/packages/cysync-core/src/constants/errors/deviceError.ts b/packages/cysync-core/src/constants/errors/deviceError.ts index de939cd85..8fd69a722 100644 --- a/packages/cysync-core/src/constants/errors/deviceError.ts +++ b/packages/cysync-core/src/constants/errors/deviceError.ts @@ -100,6 +100,8 @@ export const getDeviceErrorHandlingDetails = ( [DeviceAppErrorType.WALLET_PARTIAL_STATE]: generateErrorHandlingDetails.retry(), [DeviceAppErrorType.APP_TIMEOUT]: generateErrorHandlingDetails.retry(), + [DeviceAppErrorType.DEVICE_SESSION_INVALID]: + generateErrorHandlingDetails.retryWithReport(), [DeviceAppErrorType.CARD_OPERATION_FAILED]: generateErrorHandlingDetails.retryWithHelp('cardError'), [DeviceAppErrorType.USER_REJECTION]: generateErrorHandlingDetails.retry(), diff --git a/packages/cysync-core/src/constants/errors/serverError.ts b/packages/cysync-core/src/constants/errors/serverError.ts index 0b29874e8..d640f0fd8 100644 --- a/packages/cysync-core/src/constants/errors/serverError.ts +++ b/packages/cysync-core/src/constants/errors/serverError.ts @@ -17,8 +17,51 @@ export const getServerErrorHandlingDetails = ( ServerErrorType, ErrorHandlingDetails | undefined > = { - [ServerErrorType.UNKNOWN_ERROR]: generateErrorHandlingDetails.report(), + [ServerErrorType.UNKNOWN_ERROR]: + generateErrorHandlingDetails.retryWithReport(), [ServerErrorType.CONNOT_CONNECT]: generateErrorHandlingDetails.retry(), + + [ServerErrorType.OTP_VERIFICATION_FAILED]: + generateErrorHandlingDetails.retry(), + [ServerErrorType.LOGIN_FAILED]: generateErrorHandlingDetails.retry(), + [ServerErrorType.SIGNATURE_VERIFICATION_FAILED]: + generateErrorHandlingDetails.retry(), + [ServerErrorType.INVALID_REQUEST]: generateErrorHandlingDetails.retry(), + [ServerErrorType.UNAUTHORIZED_ACCESS]: generateErrorHandlingDetails.retry(), + [ServerErrorType.RESOURCE_NOT_FOUND]: generateErrorHandlingDetails.retry(), + [ServerErrorType.INTERNAL_SERVER_ERROR]: + generateErrorHandlingDetails.retry(), + [ServerErrorType.REQUEST_TIMEOUT]: generateErrorHandlingDetails.retry(), + [ServerErrorType.OTP_EXPIRED]: generateErrorHandlingDetails.retry(), + [ServerErrorType.PAYLOAD_VALIDATION_ERROR]: + generateErrorHandlingDetails.retry(), + [ServerErrorType.MAX_RETRIES_EXCEEDED]: + generateErrorHandlingDetails.retry(), + [ServerErrorType.ACCOUNT_LOCKED]: generateErrorHandlingDetails.retry(), + [ServerErrorType.SERVICE_UNAVAILABLE]: generateErrorHandlingDetails.retry(), + [ServerErrorType.REQUEST_CONFLICT]: generateErrorHandlingDetails.retry(), + [ServerErrorType.VALIDATION_ERROR]: + generateErrorHandlingDetails.retryWithReport(), + [ServerErrorType.MISSING_WALLET]: + generateErrorHandlingDetails.retryWithReport(), + [ServerErrorType.MISSING_USER]: + generateErrorHandlingDetails.retryWithReport(), + [ServerErrorType.MISSING_NOMINEE]: + generateErrorHandlingDetails.retryWithReport(), + [ServerErrorType.MISSING_EXECUTOR]: + generateErrorHandlingDetails.retryWithReport(), + [ServerErrorType.MISSING_SESSION]: + generateErrorHandlingDetails.retryWithReport(), + [ServerErrorType.INVALID_COUPON]: + generateErrorHandlingDetails.retryWithReport(), + [ServerErrorType.INVALID_SESSION]: + generateErrorHandlingDetails.retryWithReport(), + [ServerErrorType.INVALID_DEVICE]: + generateErrorHandlingDetails.retryWithReport(), + [ServerErrorType.INVALID_PRIVATE_KEY]: + generateErrorHandlingDetails.retryWithReport(), + [ServerErrorType.ACTIVE_PLAN_FOUND]: + generateErrorHandlingDetails.retryWithReport(), }; return serverErrorHandlingDetailsMap[errorCode]; diff --git a/packages/cysync-core/src/constants/routes/index.ts b/packages/cysync-core/src/constants/routes/index.ts index 031bed114..692f80715 100644 --- a/packages/cysync-core/src/constants/routes/index.ts +++ b/packages/cysync-core/src/constants/routes/index.ts @@ -1,3 +1,4 @@ +import { inheritance, InheritanceRouteName } from './inheritance'; import { OnboardingRouteName, onboarding } from './onboarding'; const rootRoutes = { @@ -33,12 +34,16 @@ const rootRoutes = { export const routes = { onboarding, + inheritance, ...rootRoutes, }; type RootRouteName = (typeof rootRoutes)[keyof typeof rootRoutes]['name']; -export type RouteName = RootRouteName | OnboardingRouteName; +export type RouteName = + | RootRouteName + | OnboardingRouteName + | InheritanceRouteName; export interface IRoute { name: RouteName; diff --git a/packages/cysync-core/src/constants/routes/inheritance.ts b/packages/cysync-core/src/constants/routes/inheritance.ts new file mode 100644 index 000000000..43cb37d77 --- /dev/null +++ b/packages/cysync-core/src/constants/routes/inheritance.ts @@ -0,0 +1,17 @@ +export const inheritance = { + home: { + name: 'inheritance', + path: '/inheritance', + }, + choosePlan: { + name: 'inheritance-choose-plan', + path: '/inheritance/plans', + }, + planDetails: { + name: 'inheritance-plan-details', + path: '/inheritance/plan', + }, +} as const; + +export type InheritanceRouteName = + (typeof inheritance)[keyof typeof inheritance]['name']; diff --git a/packages/cysync-core/src/context/sidebar.tsx b/packages/cysync-core/src/context/sidebar.tsx index 48b82f1d2..5a5d5a5ce 100644 --- a/packages/cysync-core/src/context/sidebar.tsx +++ b/packages/cysync-core/src/context/sidebar.tsx @@ -25,6 +25,7 @@ import { export type Page = | 'portfolio' | 'wallet' + | 'inheritance' | 'history' | 'settings' | 'help' @@ -82,6 +83,10 @@ export const SidebarProvider: React.FC = ({ dispatch(openContactSupportDialog()); return; } + if (page === 'inheritance') { + navigateTo(routes[page].home.path); + return; + } navigateTo(routes[page].path); }; @@ -93,9 +98,13 @@ export const SidebarProvider: React.FC = ({ const getState = (page: Page): State => { if (page === 'help') return State.normal; if (page === 'tutorial') return State.normal; - if (location.pathname === routes[page].path) return State.selected; + const path = + page === 'inheritance' ? routes[page].home.path : routes[page].path; + + if (location.pathname.startsWith(path)) return State.selected; return State.normal; }; + const getWalletState = (id: string | undefined): State => { if ( (id && diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/ConfirmMessage.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/ConfirmMessage.tsx new file mode 100644 index 000000000..eb1fc0f93 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/ConfirmMessage.tsx @@ -0,0 +1,96 @@ +import { SignTransactionDeviceEvent } from '@cypherock/coin-support-interfaces'; +import { + ArrowRightIcon, + Check, + confirmOnDevice, + Image, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + Throbber, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useEffect } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditEncryptedMessageDialog } from '../context'; +import { Layout } from '../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const ConfirmMessage = () => { + const lang = useAppSelector(selectLanguage); + + const strings = + lang.strings.dialogs.inheritanceEditEncryptedMessage.confirmMessage; + + const { onNext } = useInheritanceEditEncryptedMessageDialog(); + + const deviceEvents: Record = { + 0: true, + }; + + const getDeviceEventIcon = ( + loadingEvent: SignTransactionDeviceEvent, + completedEvent: SignTransactionDeviceEvent, + ) => { + if (deviceEvents[completedEvent]) return checkIconComponent; + if (deviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.actions.confirmOnDevice, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon(0, 1), + }, + { + id: '2', + text: strings.actions.verifyLocation, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon(0, 1), + }, + ]; + + return actions; + }, []); + + useEffect(() => { + const timeout = setTimeout(() => { + onNext(); + }, 2000); + + return () => clearTimeout(timeout); + }, []); + + return ( + + confirm on device + + {strings.title} + + + {actionsList.map(data => ( + + ))} + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Confirmation.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Confirmation.tsx new file mode 100644 index 000000000..5e442b730 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Confirmation.tsx @@ -0,0 +1,51 @@ +import { + Button, + Container, + LangDisplay, + Typography, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditEncryptedMessageDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Confirmation = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.dialogs.inheritanceEditEncryptedMessage; + const { onNext, onClose } = useInheritanceEditEncryptedMessageDialog(); + + return ( + + + + + } + > + + + + + + + + + + ); +}; + +export default Confirmation; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Decryption.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Decryption.tsx new file mode 100644 index 000000000..394df6ae7 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Decryption.tsx @@ -0,0 +1,113 @@ +import { SignTransactionDeviceEvent } from '@cypherock/coin-support-interfaces'; +import { + ArrowRightIcon, + Check, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + tapAnyCardDeviceAnimation2DVideo, + Throbber, + Typography, + Video, +} from '@cypherock/cysync-ui'; +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditEncryptedMessageDialog } from '../context'; +import { Layout } from '../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const Decryption = () => { + const lang = useAppSelector(selectLanguage); + + const strings = + lang.strings.dialogs.inheritanceEditEncryptedMessage.decryption.wallet; + + const { onNext } = useInheritanceEditEncryptedMessageDialog(); + const loading = false; + + const deviceEvents: Record = { + 0: true, + }; + + const getDeviceEventIcon = ( + loadingEvent: SignTransactionDeviceEvent, + completedEvent: SignTransactionDeviceEvent, + ) => { + if (deviceEvents[completedEvent]) return checkIconComponent; + if (deviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.actions.confirm, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon(0, 1), + }, + { + id: '2', + text: strings.actions.tapCard, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon(0, 1), + }, + ]; + + return actions; + }, []); + + useEffect(() => { + const timeout = setTimeout(() => { + onNext(); + }, 5000); + + return () => clearTimeout(timeout); + }, []); + + if (loading) { + return ( + + ); + } + + return ( + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/EditMessage.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/EditMessage.tsx new file mode 100644 index 000000000..95cb860d8 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/EditMessage.tsx @@ -0,0 +1,149 @@ +import { + Button, + Container, + Flex, + InputLabel, + LangDisplay, + MessageBox, + QuestionMarkButton, + TextAreaInput, + Tooltip, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useState } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditEncryptedMessageDialog } from '../context'; +import { Layout } from '../Layout'; +import { validateInputLanguage } from '~/utils'; +import { debounce } from 'lodash'; + +export const EditMessage = () => { + const lang = useAppSelector(selectLanguage); + const strings = + lang.strings.dialogs.inheritanceEditEncryptedMessage.editMessage; + const { onNext, onClose, encryptedMessage, setEncryptedMessage } = + useInheritanceEditEncryptedMessageDialog(); + + const { cardLocation } = encryptedMessage; + const { personalMessage } = encryptedMessage; + + const [cardLocationError, setCardLocationError] = useState(''); + const [personalMessageError, setPersonalMessageError] = useState(''); + + const isNextDisabled = + !personalMessage.trim() || + !cardLocation.trim() || + !cardLocationError || + !personalMessageError; + + const validateFields = debounce(() => { + const cardLocationValidation = validateInputLanguage(cardLocation, lang); + const personalMessageValidation = validateInputLanguage( + personalMessage, + lang, + ); + + if (!cardLocationValidation.success) { + setCardLocationError(cardLocationValidation.error.errors[0].message); + } else { + setCardLocationError(''); + } + + if (!personalMessageValidation.success) { + setPersonalMessageError( + personalMessageValidation.error.errors[0].message, + ); + } else { + setPersonalMessageError(''); + } + }, 300); + + useEffect(() => { + validateFields(); + + return () => { + validateFields.cancel(); + }; + }, [cardLocation, personalMessage]); + + return ( + + + + + } + > + + + + + + + + + + + + + + + + + + + + setEncryptedMessage({ ...encryptedMessage, cardLocation: text }) + } + error={cardLocationError} + /> + + + + + + + + + + + + setEncryptedMessage({ + ...encryptedMessage, + personalMessage: text, + }) + } + error={personalMessageError} + /> + + + + + ); +}; + +export default EditMessage; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Encryption.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Encryption.tsx new file mode 100644 index 000000000..a45d77ce4 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Encryption.tsx @@ -0,0 +1,122 @@ +import { SignTransactionDeviceEvent } from '@cypherock/coin-support-interfaces'; +import { + ArrowRightIcon, + Check, + Container, + Flex, + LangDisplay, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + QuestionMarkButton, + tapAnyCardDeviceAnimation2DVideo, + Throbber, + Tooltip, + Typography, + Video, +} from '@cypherock/cysync-ui'; +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditEncryptedMessageDialog } from '../context'; +import { Layout } from '../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const Encryption = () => { + const lang = useAppSelector(selectLanguage); + + const strings = + lang.strings.dialogs.inheritanceEditEncryptedMessage.encryption; + + const { onNext } = useInheritanceEditEncryptedMessageDialog(); + const loading = false; + + const deviceEvents: Record = { + 0: true, + }; + + const getDeviceEventIcon = ( + loadingEvent: SignTransactionDeviceEvent, + completedEvent: SignTransactionDeviceEvent, + ) => { + if (deviceEvents[completedEvent]) return checkIconComponent; + if (deviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.actions.enterPinAndTap, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon(0, 1), + }, + ]; + + return actions; + }, []); + + useEffect(() => { + const timeout = setTimeout(() => { + onNext(); + }, 5000); + + return () => clearTimeout(timeout); + }, []); + + if (loading) { + return ( + + ); + } + + return ( + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/FetchData.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/FetchData.tsx new file mode 100644 index 000000000..d97d8d7d7 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/FetchData.tsx @@ -0,0 +1,20 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditEncryptedMessageDialog } from '../context'; + +export const FetchData = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.dialogs.inheritanceEditEncryptedMessage.syncing; + + const { onNext, fetchData } = useInheritanceEditEncryptedMessageDialog(); + + useEffect(() => { + fetchData(); + setTimeout(() => onNext(), 2000); + }, []); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Success.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Success.tsx new file mode 100644 index 000000000..c06bbb981 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/Success.tsx @@ -0,0 +1,25 @@ +import { SuccessDialog } from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditEncryptedMessageDialog } from '../context'; + +export const Success = () => { + const lang = useAppSelector(selectLanguage); + const { onClose } = useInheritanceEditEncryptedMessageDialog(); + const strings = lang.strings.dialogs.inheritanceEditEncryptedMessage.success; + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/index.tsx new file mode 100644 index 000000000..9ce96d8c3 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Dialogs/index.tsx @@ -0,0 +1,7 @@ +export * from './Confirmation'; +export * from './FetchData'; +export * from './Decryption'; +export * from './EditMessage'; +export * from './ConfirmMessage'; +export * from './Encryption'; +export * from './Success'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Layout.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Layout.tsx new file mode 100644 index 000000000..02cebe893 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/Layout.tsx @@ -0,0 +1,45 @@ +import { + CloseButton, + DialogBox, + DialogBoxBody, + DialogBoxFooter, + DialogBoxHeader, + WidthProps, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +export interface LayoutProps { + width?: WidthProps['width']; + children: React.ReactNode; + footerComponent?: React.ReactNode; + onClose?: () => void; +} + +export const Layout: React.FC = ({ + width, + children, + footerComponent, + onClose, +}) => ( + + {onClose && ( + + + + )} + + {children} + + {footerComponent && ( + + {footerComponent} + + )} + +); + +Layout.defaultProps = { + footerComponent: undefined, + width: undefined, + onClose: undefined, +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/context/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/context/index.tsx new file mode 100644 index 000000000..16f116794 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/context/index.tsx @@ -0,0 +1,117 @@ +import React, { + Context, + Dispatch, + FC, + ReactNode, + SetStateAction, + createContext, + useContext, + useMemo, + useState, +} from 'react'; + +import { ITabs } from '~/hooks'; + +import { useInheritanceEditEncryptedMessageDialogHandler } from './useDialogHandler'; + +interface IEncryptedMessage { + cardLocation: string; + personalMessage: string; +} + +export interface InheritanceEditEncryptedMessageDialogContextInterface { + tabs: ITabs; + onNext: (tab?: number, dialog?: number) => void; + goTo: (tab: number, dialog?: number) => void; + onPrevious: () => void; + onClose: () => void; + currentTab: number; + currentDialog: number; + isDeviceRequired: boolean; + unhandledError?: any; + fetchData: () => void; + encryptedMessage: IEncryptedMessage; + setEncryptedMessage: Dispatch>; +} + +export const InheritanceEditEncryptedMessageDialogContext: Context = + createContext( + {} as InheritanceEditEncryptedMessageDialogContextInterface, + ); + +export interface InheritanceEditEncryptedMessageDialogContextProviderProps { + children: ReactNode; +} + +export const InheritanceEditEncryptedMessageDialogProvider: FC< + InheritanceEditEncryptedMessageDialogContextProviderProps +> = ({ children }) => { + const { + currentDialog, + currentTab, + goTo, + isDeviceRequired, + onClose, + onNext, + onPrevious, + tabs, + } = useInheritanceEditEncryptedMessageDialogHandler(); + + const [encryptedMessage, setEncryptedMessage] = useState({ + cardLocation: '', + personalMessage: '', + }); + + const fetchData = () => { + 'Implement this function'; + + const dummy = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque id ullamcorper dui, sed vestibulum libero. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed placerat nibh sed justo sagittis venenatis. Nullam dictum ipsum ac nunc aliquet, ut condimentum nibh pharetra. Pellentesque interdum dignissim blandit. Nullam ac tincidunt lacus. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Vivamus magna velit, pulvinar euismod nisi non, venenatis vehicula justo. Morbi ligula purus, condimentum vitae eleifend ut, mattis at diam. Sed non pulvinar ex.'; + setEncryptedMessage({ + cardLocation: dummy, + personalMessage: dummy, + }); + setTimeout(() => { + onNext(); + }, 2000); + }; + + const ctx = useMemo( + () => ({ + onNext, + onPrevious, + tabs, + onClose, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + encryptedMessage, + setEncryptedMessage, + fetchData, + }), + [ + onNext, + onPrevious, + tabs, + onClose, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + encryptedMessage, + setEncryptedMessage, + fetchData, + ], + ); + + return ( + + {children} + + ); +}; + +export function useInheritanceEditEncryptedMessageDialog(): InheritanceEditEncryptedMessageDialogContextInterface { + return useContext(InheritanceEditEncryptedMessageDialogContext); +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/context/useDialogHandler.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/context/useDialogHandler.tsx new file mode 100644 index 000000000..7b76aea33 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/context/useDialogHandler.tsx @@ -0,0 +1,123 @@ +import React, { useMemo } from 'react'; + +import { ITabs, useTabsAndDialogs } from '~/hooks'; +import { useAppDispatch, closeDialog } from '~/store'; + +import { + Confirmation, + FetchData, + Decryption, + EditMessage, + ConfirmMessage, + Encryption, + Success, +} from '../Dialogs'; + +export const tabIndicies = { + confirmation: { + tabNumber: 0, + dialogs: { + confirmation: 0, + }, + }, + fetchData: { + tabNumber: 1, + dialogs: { + fetchData: 0, + }, + }, + decryption: { + tabNumber: 2, + dialogs: { + decryption: 0, + }, + }, + editMessage: { + tabNumber: 3, + dialogs: { + editMessage: 0, + }, + }, + confirmMessage: { + tabNumber: 4, + dialogs: { + confirmMessage: 0, + }, + }, + encryption: { + tabNumber: 5, + dialogs: { + encryption: 0, + }, + }, + success: { + tabNumber: 6, + dialogs: { + success: 0, + }, + }, +}; + +export const useInheritanceEditEncryptedMessageDialogHandler = () => { + const dispatch = useAppDispatch(); + + const deviceRequiredDialogsMap: Record = + useMemo( + () => ({ + [tabIndicies.fetchData.tabNumber]: [ + tabIndicies.fetchData.dialogs.fetchData, + ], + [tabIndicies.encryption.tabNumber]: [ + tabIndicies.encryption.dialogs.encryption, + ], + }), + [], + ); + + const tabs: ITabs = [ + { name: 'Confirmation', dialogs: [] }, + { name: 'Fetch Data', dialogs: [] }, + { name: 'Decryption', dialogs: [] }, + { name: 'Edit Message', dialogs: [] }, + { + name: 'Confirm Message', + dialogs: [], + }, + { + name: 'Encryption', + dialogs: [], + }, + { + name: 'Success', + dialogs: [], + }, + ]; + + const { + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + } = useTabsAndDialogs({ + deviceRequiredDialogsMap, + tabs, + dialogName: 'inheritanceEditEncryptedMessage', + }); + + const onClose = () => { + dispatch(closeDialog('inheritanceEditEncryptedMessage')); + }; + + return { + onClose, + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + tabs, + }; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/index.tsx new file mode 100644 index 000000000..2ab1d4bb8 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditEncryptedMessage/index.tsx @@ -0,0 +1,64 @@ +import { BlurOverlay } from '@cypherock/cysync-ui'; +import React, { FC } from 'react'; + +import { + ErrorHandlerDialog, + WithConnectedDevice, + WithConnectedDeviceProps, +} from '~/components'; + +import { + InheritanceEditEncryptedMessageDialogProvider, + useInheritanceEditEncryptedMessageDialog, +} from './context'; + +const DeviceConnectionWrapper: React.FC<{ + isDeviceRequired: boolean; + children: React.ReactNode; + withConnectedProps?: WithConnectedDeviceProps; +}> = ({ isDeviceRequired, withConnectedProps, children }) => { + if (isDeviceRequired) + return ( + + {children} + + ); + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; +}; + +DeviceConnectionWrapper.defaultProps = { + withConnectedProps: {}, +}; + +const InheritanceEditEncryptedMessage: FC = () => { + const { + tabs, + currentTab, + currentDialog, + unhandledError, + onClose, + isDeviceRequired, + } = useInheritanceEditEncryptedMessageDialog(); + + return ( + + + + {tabs[currentTab]?.dialogs[currentDialog]} + + + + ); +}; + +export const InheritanceEditEncryptedMessageDialog = () => ( + + + +); diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/EditMessage.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/EditMessage.tsx new file mode 100644 index 000000000..0ee620940 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/EditMessage.tsx @@ -0,0 +1,98 @@ +import { + Button, + CloseButton, + DialogBox, + DialogBoxBody, + DialogBoxFooter, + DialogBoxHeader, + Flex, + LangDisplay, + ScrollableContainer, + TextAreaInput, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditExecutorMessageDialog } from '../context'; + +export const EditMessage = () => { + const lang = useAppSelector(selectLanguage); + + const { + onClose, + onNext, + updateExecutorMessage, + isUpdatingExecutorMessage, + isUpdateExecutorMessageCompleted, + executorMessage, + setExecutorMessage, + } = useInheritanceEditExecutorMessageDialog(); + const strings = + lang.strings.dialogs.inheritanceEditExecutorMessage.editMessage; + const { form } = strings; + + const handleUpdate = () => { + updateExecutorMessage(); + }; + + useEffect(() => { + if (isUpdateExecutorMessageCompleted) onNext(); + }, [isUpdateExecutorMessageCompleted]); + + if (isUpdatingExecutorMessage) { + return ( + + ); + } + + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/FetchData.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/FetchData.tsx new file mode 100644 index 000000000..c3be89480 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/FetchData.tsx @@ -0,0 +1,29 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditExecutorMessageDialog } from '../context'; + +export const FetchData = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.dialogs.inheritanceEditExecutorMessage.fetchData; + const { + fetchExecutorMessage, + retryIndex, + isFetchExecutorMessageCompleted, + onNext, + } = useInheritanceEditExecutorMessageDialog(); + + useEffect(() => { + fetchExecutorMessage(); + }, [retryIndex]); + + useEffect(() => { + if (isFetchExecutorMessageCompleted) { + onNext(); + } + }, [isFetchExecutorMessageCompleted]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/Success.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/Success.tsx new file mode 100644 index 000000000..9a3828988 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/Success.tsx @@ -0,0 +1,25 @@ +import { SuccessDialog } from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditExecutorMessageDialog } from '../context'; + +export const Success = () => { + const lang = useAppSelector(selectLanguage); + const { onClose } = useInheritanceEditExecutorMessageDialog(); + const { strings } = lang; + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/index.tsx new file mode 100644 index 000000000..4ee466b93 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/Dialogs/index.tsx @@ -0,0 +1,3 @@ +export * from './EditMessage'; +export * from './FetchData'; +export * from './Success'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/context/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/context/index.tsx new file mode 100644 index 000000000..cec31cce9 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/context/index.tsx @@ -0,0 +1,246 @@ +import { assert } from '@cypherock/cysync-utils'; +import React, { + Context, + FC, + createContext, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; + +import { ITabs, useAsync, useMemoReturn, useTabsAndDialogs } from '~/hooks'; +import { inheritanceEditPlansService } from '~/services'; +import { inheritanceRecoverPlansService } from '~/services/inheritance/plan/recover'; +import { AuthTokenConfig } from '~/services/utils'; +import { + closeDialog, + IWalletAuthTokens, + selectInheritanceSeedAuthTokens, + updateWalletAuthTokens, + useAppDispatch, + useAppSelector, +} from '~/store'; + +import { + InheritanceEditExecutorMessageDialogContextInterface, + InheritanceEditExecutorMessageDialogContextProviderProps, + tabIndicies, +} from './types'; + +import { useSession } from '../../hooks'; +import { FetchData, EditMessage, Success } from '../Dialogs'; + +export * from './types'; + +export const InheritanceEditExecutorMessageDialogContext: Context = + createContext( + {} as InheritanceEditExecutorMessageDialogContextInterface, + ); + +export const InheritanceEditExecutorMessageDialogProvider: FC< + InheritanceEditExecutorMessageDialogContextProviderProps +> = ({ children, walletId }) => { + const dispatch = useAppDispatch(); + + const deviceRequiredDialogsMap: Record = + useMemo( + () => ({ + [tabIndicies.fetchdata.tabNumber]: [ + tabIndicies.fetchdata.dialogs.fetchdata, + ], + [tabIndicies.editmessage.tabNumber]: [ + tabIndicies.editmessage.dialogs.editmessage, + ], + }), + [], + ); + + const tabs: ITabs = [ + { + name: 'Fetch Data', + dialogs: [], + }, + { + name: 'Edit Message', + dialogs: [], + }, + { + name: 'Success Message', + dialogs: [], + }, + ]; + + const [executorMessage, setExecutorMessage] = useState(); + const [unhandledError, setUnhandledError] = useState(); + const [retryIndex, setRetryIndex] = useState(0); + + const onError = useCallback((e?: any) => { + setUnhandledError(e); + }, []); + + const walletAuthTokens = useAppSelector(selectInheritanceSeedAuthTokens); + const sessionService = useSession(onError); + + const fetchData = useCallback(async () => { + let sessionId = await sessionService.getIsActive(); + const accessToken = walletAuthTokens[walletId]?.accessToken; + const refreshToken = walletAuthTokens[walletId]?.refreshToken; + + if (!sessionId) { + sessionId = await sessionService.start(); + } + + assert(sessionId, 'sessionId not found'); + assert(accessToken, 'accessToken not found'); + assert(refreshToken, 'refreshToken not found'); + + const updateAuthToken = (token: string) => { + const authTokens: IWalletAuthTokens = { + accessToken: token, + refreshToken, + }; + + updateWalletAuthTokens({ walletId, authTokens }); + }; + + const authTokenConfig: AuthTokenConfig = { + accessToken, + refreshTokenConfig: { + refreshToken, + updateAuthToken, + }, + }; + + const response = await inheritanceRecoverPlansService.recover({ + authTokenConfig, + sessionId, + executor: true, + }); + + if (response.error) throw response.error; + + setExecutorMessage(response.result.executorMessage ?? ''); + return true; + }, [ + sessionService.start, + sessionService.getIsActive, + walletAuthTokens[walletId], + ]); + + const [ + fetchExecutorMessage, + isFetchingExecutorMessage, + isFetchExecutorMessageCompleted, + resetFetchExecutorMessage, + ] = useAsync(fetchData, onError); + + const updateData = useCallback(async () => { + if (!executorMessage) return false; + let sessionId = await sessionService.getIsActive(); + const accessToken = walletAuthTokens[walletId]?.accessToken; + const refreshToken = walletAuthTokens[walletId]?.refreshToken; + + if (!sessionId) { + sessionId = await sessionService.start(); + } + + assert(accessToken, 'accessToken not found'); + assert(sessionId, 'sessionId not found'); + assert(refreshToken, 'refreshToken not found'); + + const updateAuthToken = (token: string) => { + const authTokens: IWalletAuthTokens = { + accessToken: token, + refreshToken, + }; + + updateWalletAuthTokens({ walletId, authTokens }); + }; + + const authTokenConfig: AuthTokenConfig = { + accessToken, + refreshTokenConfig: { + refreshToken, + updateAuthToken, + }, + }; + + const response = await inheritanceEditPlansService.updateExecutorMessage({ + authTokenConfig, + sessionId, + executorMessage, + }); + if (response.error) throw response.error; + return true; + }, [executorMessage, walletAuthTokens[walletId]]); + + const [ + updateExecutorMessage, + isUpdatingExecutorMessage, + isUpdateExecutorMessageCompleted, + resetUpdateExecutorMessage, + ] = useAsync(updateData, onError); + + const { + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + } = useTabsAndDialogs({ + deviceRequiredDialogsMap, + tabs, + dialogName: 'inheritanceEditExecutorMessage', + }); + + const onClose = () => { + dispatch(closeDialog('inheritanceEditExecutorMessage')); + }; + + const resetAll = useCallback(() => { + resetFetchExecutorMessage(); + resetUpdateExecutorMessage(); + setExecutorMessage(undefined); + setRetryIndex(v => v + 1); + }, []); + + const onRetry = useCallback(() => { + resetAll(); + goTo(0, 0); + setUnhandledError(undefined); + }, [currentTab, currentDialog]); + + const ctx = useMemoReturn({ + onNext, + onPrevious, + tabs, + onClose, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + unhandledError, + onRetry, + retryIndex, + fetchExecutorMessage, + updateExecutorMessage, + executorMessage, + setExecutorMessage, + isFetchingExecutorMessage, + isFetchExecutorMessageCompleted, + isUpdatingExecutorMessage, + isUpdateExecutorMessageCompleted, + }); + + return ( + + {children} + + ); +}; + +export function useInheritanceEditExecutorMessageDialog(): InheritanceEditExecutorMessageDialogContextInterface { + return useContext(InheritanceEditExecutorMessageDialogContext); +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/context/types.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/context/types.tsx new file mode 100644 index 000000000..265d18d50 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/context/types.tsx @@ -0,0 +1,55 @@ +import { Dispatch, ReactNode } from 'react'; + +import { ITabs } from '~/hooks'; + +export interface InheritanceEditExecutorMessageDialogContextInterface { + tabs: ITabs; + onNext: (tab?: number, dialog?: number) => void; + goTo: (tab: number, dialog?: number) => void; + onPrevious: () => void; + onClose: () => void; + currentTab: number; + currentDialog: number; + isDeviceRequired: boolean; + onRetry: () => void; + retryIndex: number; + unhandledError?: any; + fetchExecutorMessage: () => void; + updateExecutorMessage: () => void; + executorMessage?: string; + setExecutorMessage: Dispatch>; + isFetchingExecutorMessage: boolean; + isFetchExecutorMessageCompleted: boolean; + isUpdatingExecutorMessage: boolean; + isUpdateExecutorMessageCompleted: boolean; +} + +export interface InheritanceEditExecutorMessageDialogProps { + walletId: string; +} + +export interface InheritanceEditExecutorMessageDialogContextProviderProps + extends InheritanceEditExecutorMessageDialogProps { + children: ReactNode; +} + +export const tabIndicies = { + fetchdata: { + tabNumber: 0, + dialogs: { + fetchdata: 0, + }, + }, + editmessage: { + tabNumber: 1, + dialogs: { + editmessage: 0, + }, + }, + successmessage: { + tabNumber: 2, + dialogs: { + successmessage: 0, + }, + }, +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/index.tsx new file mode 100644 index 000000000..76aba960a --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditExecutorMessage/index.tsx @@ -0,0 +1,67 @@ +import { BlurOverlay } from '@cypherock/cysync-ui'; +import React, { FC } from 'react'; + +import { + ErrorHandlerDialog, + WithConnectedDevice, + WithConnectedDeviceProps, +} from '~/components'; + +import { + InheritanceEditExecutorMessageDialogProps, + InheritanceEditExecutorMessageDialogProvider, + useInheritanceEditExecutorMessageDialog, +} from './context'; + +const DeviceConnectionWrapper: React.FC<{ + isDeviceRequired: boolean; + children: React.ReactNode; + withConnectedProps?: WithConnectedDeviceProps; +}> = ({ isDeviceRequired, withConnectedProps, children }) => { + if (isDeviceRequired) + return ( + + {children} + + ); + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; +}; + +DeviceConnectionWrapper.defaultProps = { + withConnectedProps: {}, +}; + +const InheritanceEditExecutorMessage: FC = () => { + const { + tabs, + currentTab, + currentDialog, + unhandledError, + onClose, + isDeviceRequired, + } = useInheritanceEditExecutorMessageDialog(); + + return ( + + + + {tabs[currentTab]?.dialogs[currentDialog]} + + + + ); +}; + +export const InheritanceEditExecutorMessageDialog: FC< + InheritanceEditExecutorMessageDialogProps +> = props => ( + + + +); diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/FetchData.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/FetchData.tsx new file mode 100644 index 000000000..db08d19c1 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/FetchData.tsx @@ -0,0 +1,18 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditReminderTimeDialog } from '../context'; + +export const FetchData = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.dialogs.inheritanceEditReminderTime.fetchData; + const { fetchData } = useInheritanceEditReminderTimeDialog(); + + useEffect(() => { + fetchData(); + }, []); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/ReminderSetup.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/ReminderSetup.tsx new file mode 100644 index 000000000..6f1229a71 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/ReminderSetup.tsx @@ -0,0 +1,117 @@ +import { + Accordion, + Button, + CloseButton, + DialogBox, + DialogBoxBody, + DialogBoxFooter, + DialogBoxHeader, + Flex, + InputLabel, + LangDisplay, + MessageBox, + Reminder, + ScrollableContainer, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useState } from 'react'; + +import { LoaderDialog } from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditReminderTimeDialog } from '../context'; + +export const ReminderSetup = () => { + const lang = useAppSelector(selectLanguage); + const [loading, setLoading] = useState(false); + const { onClose, updateData, reminder } = + useInheritanceEditReminderTimeDialog(); + const [newReminder, setNewReminder] = useState(reminder ?? 1); + const strings = + lang.strings.dialogs.inheritanceEditReminderTime.reminderSetup; + const { form } = strings; + + if (loading) { + return ( + + ); + } + + return ( + + + + + + + + + + + + + + + + + + + setNewReminder(1)} + /> + setNewReminder(3)} + /> + setNewReminder(6)} + /> + + + + + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/Success.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/Success.tsx new file mode 100644 index 000000000..44dcdfb3f --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/Success.tsx @@ -0,0 +1,25 @@ +import { SuccessDialog } from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditReminderTimeDialog } from '../context'; + +export const Success = () => { + const lang = useAppSelector(selectLanguage); + const { onClose } = useInheritanceEditReminderTimeDialog(); + const strings = lang.strings.dialogs.inheritanceEditReminderTime.success; + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/index.tsx new file mode 100644 index 000000000..79ed52f73 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/Dialogs/index.tsx @@ -0,0 +1,3 @@ +export * from './FetchData'; +export * from './Success'; +export * from './ReminderSetup'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/context/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/context/index.tsx new file mode 100644 index 000000000..cb2abdddd --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/context/index.tsx @@ -0,0 +1,161 @@ +import React, { + Context, + FC, + ReactNode, + createContext, + useContext, + useState, +} from 'react'; + +import { ITabs, useMemoReturn, useTabsAndDialogs } from '~/hooks'; +import { inheritanceLoginService, inheritancePlanService } from '~/services'; +import { + closeDialog, + reminderPeriodNumToStringMap, + reminderPeriodStringToNumMap, + useAppDispatch, +} from '~/store'; +import { useAuthTokenConfig } from '../../hooks/useAuthConfig'; +import { useCaptureUnhandledErrors } from '../../hooks/useCatpureUnhandledErrors'; +import { useUpdatePlanDetails } from '../../hooks/useUpdatePlanDetails'; + +import { FetchData, ReminderSetup, Success } from '../Dialogs'; + +export interface InheritanceEditReminderTimeDialogContextInterface { + tabs: ITabs; + onNext: (tab?: number, dialog?: number) => void; + goTo: (tab: number, dialog?: number) => void; + onPrevious: () => void; + onClose: () => void; + currentTab: number; + currentDialog: number; + isDeviceRequired: boolean; + unhandledError?: any; + fetchData: () => void; + updateData: (n: number) => void; + reminder?: number; + onRetry?: () => void; +} + +export const InheritanceEditReminderTimeDialogContext: Context = + createContext( + {} as InheritanceEditReminderTimeDialogContextInterface, + ); + +export interface InheritanceEditReminderTimeDialogProps { + walletId: string; +} + +export interface InheritanceEditReminderTimeDialogContextProviderProps + extends InheritanceEditReminderTimeDialogProps { + children: ReactNode; +} + +export const InheritanceEditReminderTimeDialogProvider: FC< + InheritanceEditReminderTimeDialogContextProviderProps +> = ({ children, walletId }) => { + const dispatch = useAppDispatch(); + + const deviceRequiredDialogsMap: Record = {}; + const tabs: ITabs = [ + { + name: 'Fetch Data', + dialogs: [], + }, + { + name: 'Reminder Setup', + dialogs: [], + }, + { + name: 'Success Message', + dialogs: [], + }, + ]; + + const [reminder, setReminder] = useState(); + const { unhandledError, setUnhandledError, captureErrors } = + useCaptureUnhandledErrors(); + + const { + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + } = useTabsAndDialogs({ + deviceRequiredDialogsMap, + tabs, + dialogName: 'inheritanceEditReminderTime', + }); + + const { authTokenConfig } = useAuthTokenConfig({ + walletId, + authType: 'SEED', + }); + const { fetchAndUpdatePlan } = useUpdatePlanDetails({ authTokenConfig }); + + const onClose = () => { + fetchAndUpdatePlan(); + dispatch(closeDialog('inheritanceEditReminderTime')); + }; + + const onRetry = () => { + setUnhandledError(undefined); + goTo(0, 0); + }; + + const fetchData = captureErrors(async () => { + const result = await inheritancePlanService.getPlan({ authTokenConfig }); + + const reminderValue = + reminderPeriodStringToNumMap[ + result?.result?.emailConfig?.frequency ?? '' + ] ?? 1; + + setReminder(reminderValue); + onNext(); + }); + + const updateData = captureErrors(async (newDuration: number) => { + const frequency = reminderPeriodNumToStringMap[newDuration] ?? 'monthly'; + + const result = await inheritanceLoginService.updateReminder({ + frequency, + authTokenConfig, + }); + + if (result.error) { + throw result.error ?? "Couldn't update reminder"; + } + + setReminder(newDuration); + onNext(); + }); + + const ctx = useMemoReturn({ + onNext, + onPrevious, + tabs, + onClose, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + fetchData, + updateData, + reminder, + unhandledError, + onRetry, + }); + + return ( + + {children} + + ); +}; + +export function useInheritanceEditReminderTimeDialog(): InheritanceEditReminderTimeDialogContextInterface { + return useContext(InheritanceEditReminderTimeDialogContext); +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/index.tsx new file mode 100644 index 000000000..381ba62bf --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditReminderTime/index.tsx @@ -0,0 +1,37 @@ +import { BlurOverlay } from '@cypherock/cysync-ui'; +import React, { FC } from 'react'; + +import { ErrorHandlerDialog } from '~/components'; + +import { + InheritanceEditReminderTimeDialogProps, + InheritanceEditReminderTimeDialogProvider, + useInheritanceEditReminderTimeDialog, +} from './context'; + +const InheritanceEditReminderTime: FC = () => { + const { tabs, currentTab, currentDialog, unhandledError, onClose, onRetry } = + useInheritanceEditReminderTimeDialog(); + + return ( + + + {tabs[currentTab]?.dialogs[currentDialog]} + + + ); +}; + +export const InheritanceEditReminderTimeDialog: FC< + InheritanceEditReminderTimeDialogProps +> = props => ( + + + +); diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/EditDetails.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/EditDetails.tsx new file mode 100644 index 000000000..9871ca2f7 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/EditDetails.tsx @@ -0,0 +1,138 @@ +import { + Button, + CloseButton, + Container, + DialogBox, + DialogBoxBody, + DialogBoxFooter, + DialogBoxHeader, + Input, + LangDisplay, + ScrollableContainer, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useState } from 'react'; + +import { useAppSelector } from '~/store'; +import { selectLanguage } from '~/store/lang'; + +import { useInheritanceEditUserDetailsDialog } from '../context'; +import { constants } from '@cypherock/cysync-core-constants'; + +export const EditDetails = () => { + const lang = useAppSelector(selectLanguage); + const { onClose, onUserDetailsSubmit, isSubmittingUserDetails, userType } = + useInheritanceEditUserDetailsDialog(); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [alternateEmail, setAlternateEmail] = useState(''); + + const strings = lang.strings.dialogs.inheritanceEditUserDetails.editDetails; + const { form } = lang.strings.inheritance.dialog.userDetails; + + const formId = 'inheritance-edit-user-details'; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (isSubmittingUserDetails) return; + + onUserDetailsSubmit({ + name, + email, + alternateEmail, + }); + }; + + return ( + + + + + + +
+ + + + + + + + + + + + +
+
+
+ + + + +
+ ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/Success.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/Success.tsx new file mode 100644 index 000000000..07f7c35f3 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/Success.tsx @@ -0,0 +1,25 @@ +import { parseLangTemplate, SuccessDialog } from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditUserDetailsDialog } from '../context'; + +export const Success = () => { + const lang = useAppSelector(selectLanguage); + const { onClose, userType } = useInheritanceEditUserDetailsDialog(); + const strings = lang.strings.dialogs.inheritanceEditUserDetails.success; + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/VerifyOTP.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/VerifyOTP.tsx new file mode 100644 index 000000000..e70f44e5f --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/VerifyOTP.tsx @@ -0,0 +1,82 @@ +import { sleep } from '@cypherock/sdk-utils'; +import React, { useMemo, useRef, useState } from 'react'; + +import { LoaderDialog } from '~/components'; +import { + OTPInputDialog, + OTPInputDialogRef, +} from '~/dialogs/Inheritance/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEditUserDetailsDialog } from '../context'; + +export const VerifyOTP: React.FC = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritance.dialog.verifyOTP; + + const { onClose, onPrevious, userDetails, onNext } = + useInheritanceEditUserDetailsDialog(); + + const [email, setEmail] = useState(userDetails?.email ?? ''); + const [title, setTitle] = useState(strings.primaryEmailOTP.title); + const [loading, setLoading] = useState(false); + + const otpRef = useRef(null); + + const onVerify = async () => { + // DUMMY FUNCTION + if (email === userDetails?.alternateEmail) { + setLoading(true); + await sleep(2000); + onNext(); + return; + } + + setEmail(userDetails?.alternateEmail ?? ''); + setTitle(strings.alternateEmailOTP.title); + otpRef.current?.reset(); + }; + + const onResend = () => { + // DUMMY FUNCTION + }; + + const otpExpireTime = useMemo( + () => new Date(Date.now() + 60 * 1000).toISOString(), + [email], + ); + const otpLength = 6; + const retriesRemaining = 3; + + if (loading) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/index.tsx new file mode 100644 index 000000000..f8b647c4b --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/Dialogs/index.tsx @@ -0,0 +1,3 @@ +export * from './EditDetails'; +export * from './VerifyOTP'; +export * from './Success'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/context/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/context/index.tsx new file mode 100644 index 000000000..5c27cebc6 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/context/index.tsx @@ -0,0 +1,137 @@ +import { sleep } from '@cypherock/sdk-utils'; +import React, { + Context, + FC, + ReactNode, + createContext, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; + +import { ITabs, useTabsAndDialogs } from '~/hooks'; +import { closeDialog, useAppDispatch } from '~/store'; + +import { + InheritanceEditUserDetailsUserType, + InheritanceEditUserDetailsDialogProps, +} from '..'; +import { EditDetails, Success, VerifyOTP } from '../Dialogs'; + +export interface IUserDetails { + name: string; + email: string; + alternateEmail: string; +} + +export interface InheritanceEditUserDetailsDialogContextInterface { + tabs: ITabs; + onNext: (tab?: number, dialog?: number) => void; + goTo: (tab: number, dialog?: number) => void; + onPrevious: () => void; + onClose: () => void; + currentTab: number; + currentDialog: number; + isDeviceRequired: boolean; + unhandledError?: any; + userDetails?: IUserDetails; + onUserDetailsSubmit: (params: IUserDetails) => void; + isSubmittingUserDetails: boolean; + userType: InheritanceEditUserDetailsUserType; +} + +export const InheritanceEditUserDetailsDialogContext: Context = + createContext( + {} as InheritanceEditUserDetailsDialogContextInterface, + ); + +export interface InheritanceEditUserDetailsDialogContextProviderProps + extends InheritanceEditUserDetailsDialogProps { + children: ReactNode; +} + +export const InheritanceEditUserDetailsDialogProvider: FC< + InheritanceEditUserDetailsDialogContextProviderProps +> = ({ children, userType }) => { + const dispatch = useAppDispatch(); + + const [isSubmittingUserDetails, setIsSubmittingUserDetails] = useState(false); + const [userDetails, setUserDetails] = useState(); + + const deviceRequiredDialogsMap: Record = {}; + const tabs: ITabs = [ + { + name: 'Edit Details', + dialogs: [], + }, + { name: 'Verify OTP', dialogs: [] }, + { name: 'Success', dialogs: [] }, + ]; + + const { + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + } = useTabsAndDialogs({ + deviceRequiredDialogsMap, + tabs, + dialogName: 'inheritanceEditUserDetails', + }); + + const onClose = () => { + dispatch(closeDialog('inheritanceEditUserDetails')); + }; + + const onUserDetailsSubmit = useCallback(async (params: IUserDetails) => { + setIsSubmittingUserDetails(true); + setUserDetails(params); + await sleep(2000); + setIsSubmittingUserDetails(false); + goTo(1); + }, []); + + const ctx = useMemo( + () => ({ + onNext, + onPrevious, + tabs, + onClose, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + userDetails, + onUserDetailsSubmit, + isSubmittingUserDetails, + userType, + }), + [ + onNext, + onPrevious, + tabs, + onClose, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + userDetails, + onUserDetailsSubmit, + isSubmittingUserDetails, + userType, + ], + ); + + return ( + + {children} + + ); +}; + +export function useInheritanceEditUserDetailsDialog(): InheritanceEditUserDetailsDialogContextInterface { + return useContext(InheritanceEditUserDetailsDialogContext); +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/index.tsx new file mode 100644 index 000000000..38b7b9b3a --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EditUserDetails/index.tsx @@ -0,0 +1,44 @@ +import { BlurOverlay } from '@cypherock/cysync-ui'; +import React, { FC } from 'react'; + +import { ErrorHandlerDialog } from '~/components'; + +import { + InheritanceEditUserDetailsDialogProvider, + useInheritanceEditUserDetailsDialog, +} from './context'; + +export type InheritanceEditUserDetailsUserType = + | 'owner' + | 'nominee' + | 'executor'; + +export interface InheritanceEditUserDetailsDialogProps { + userType: InheritanceEditUserDetailsUserType; +} + +const InheritanceEditUserDetails = () => { + const { tabs, currentTab, currentDialog, unhandledError, onClose } = + useInheritanceEditUserDetailsDialog(); + + return ( + + + {tabs[currentTab]?.dialogs[currentDialog]} + + + ); +}; + +export const InheritanceEditUserDetailsDialog: FC< + InheritanceEditUserDetailsDialogProps +> = props => ( + + + +); diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/AuthenticateClearData.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/AuthenticateClearData.tsx new file mode 100644 index 000000000..ac4fd4d6c --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/AuthenticateClearData.tsx @@ -0,0 +1,38 @@ +import { + DialogBox, + GuidedFlowDialogBox, + tapAnyCardDeviceAnimation2DVideo, + Video, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../../context'; + +export const AuthenticateClearData = () => { + const lang = useAppSelector(selectLanguage); + const strings = + lang.strings.dialogs.inheritanceEstateRecovery.instructions.dialogs; + const { onNext, onPrevious } = useInheritanceEstateRecoveryDialog(); + return ( + + + } + onNext={() => onNext()} + onPrevious={() => onPrevious()} + title={strings.tapCards.title} + subtitle={strings.tapCards.subTitle} + messageBoxList={strings.tapCards.messageBoxList} + /> + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/ClearData.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/ClearData.tsx new file mode 100644 index 000000000..3f77fd598 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/ClearData.tsx @@ -0,0 +1,28 @@ +import { + clearDeviceData, + DialogBox, + GuidedFlowDialogBox, + Image, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../../context'; + +export const ClearData = () => { + const lang = useAppSelector(selectLanguage); + const strings = + lang.strings.dialogs.inheritanceEstateRecovery.instructions.dialogs; + const { onNext, onPrevious } = useInheritanceEstateRecoveryDialog(); + return ( + + } + onNext={() => onNext()} + onPrevious={() => onPrevious()} + title={strings.clearData.title} + /> + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/Confirm.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/Confirm.tsx new file mode 100644 index 000000000..3501b5c7b --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/Confirm.tsx @@ -0,0 +1,28 @@ +import { + clearDeviceData, + DialogBox, + GuidedFlowDialogBox, + Image, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../../context'; + +export const ConfirmClearData = () => { + const lang = useAppSelector(selectLanguage); + const strings = + lang.strings.dialogs.inheritanceEstateRecovery.instructions.dialogs; + const { onNext, onPrevious } = useInheritanceEstateRecoveryDialog(); + return ( + + } + onNext={() => onNext()} + onPrevious={() => onPrevious()} + title={strings.confirmClearData.title} + /> + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/Settings.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/Settings.tsx new file mode 100644 index 000000000..11584d778 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/Settings.tsx @@ -0,0 +1,30 @@ +import { + confirmDeviceSettings, + DialogBox, + GuidedFlowDialogBox, + Image, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../../context'; + +export const Settings = () => { + const lang = useAppSelector(selectLanguage); + const strings = + lang.strings.dialogs.inheritanceEstateRecovery.instructions.dialogs; + const { onNext, onPrevious } = useInheritanceEstateRecoveryDialog(); + return ( + + + } + onNext={() => onNext()} + onPrevious={() => onPrevious()} + title={strings.settings.title} + /> + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/index.tsx new file mode 100644 index 000000000..9478da627 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ClearDeviceData/index.tsx @@ -0,0 +1,4 @@ +export * from './ClearData'; +export * from './Confirm'; +export * from './Settings'; +export * from './AuthenticateClearData'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Decrypt.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Decrypt.tsx new file mode 100644 index 000000000..9de243ec3 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Decrypt.tsx @@ -0,0 +1,125 @@ +import { InheritanceDecryptMessageDeviceEvent } from '@cypherock/app-support-inheritance'; +import { + ArrowRightIcon, + Check, + Container, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + tapAnyCardDeviceAnimation2DVideo, + Throbber, + Typography, + Video, +} from '@cypherock/cysync-ui'; +import React, { useEffect } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../context'; +import { Layout } from '../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const DecryptMessage = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.dialogs.inheritanceEstateRecovery.decryption; + + const { + onNext, + retryIndex, + clearErrors, + decryptPinStart, + decryptPinDeviceEvents, + decryptPinAbort, + } = useInheritanceEstateRecoveryDialog(); + + const getDeviceEventIcon = ( + loadingEvent: InheritanceDecryptMessageDeviceEvent, + completedEvent: InheritanceDecryptMessageDeviceEvent, + ) => { + if (decryptPinDeviceEvents[completedEvent]) return checkIconComponent; + if (decryptPinDeviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.device.actions.confirm, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceDecryptMessageDeviceEvent.INIT, + InheritanceDecryptMessageDeviceEvent.CONFIRMED, + ), + }, + { + id: '2', + text: strings.device.actions.tapCard, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceDecryptMessageDeviceEvent.CONFIRMED, + InheritanceDecryptMessageDeviceEvent.CARD_TAPPED, + ), + }, + ]; + + return actions; + }, [decryptPinDeviceEvents]); + + useEffect(() => { + clearErrors(); + decryptPinStart(); + + return () => { + decryptPinAbort(); + }; + }, [retryIndex, clearErrors]); + + useEffect(() => { + if ( + decryptPinDeviceEvents[InheritanceDecryptMessageDeviceEvent.CARD_TAPPED] + ) { + setTimeout(() => onNext(), 2000); + } + }, [decryptPinDeviceEvents]); + + return ( + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Message.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Message.tsx new file mode 100644 index 000000000..d3545b897 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Message.tsx @@ -0,0 +1,74 @@ +import { + Button, + CheckBox, + Clipboard, + Container, + LangDisplay, + QuestionMarkButton, + TextAreaInput, + Tooltip, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useState } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Message = () => { + const lang = useAppSelector(selectLanguage); + const { onNext, decryptedCardLocation, decryptedPersonalMessage } = + useInheritanceEstateRecoveryDialog(); + const [isChecked, setIsChecked] = useState(false); + const strings = lang.strings.dialogs.inheritanceEstateRecovery.viewMessage; + + const userName = 'Alfred Ballows'; + const cardLocation = decryptedCardLocation ?? ''; + const personalMessage = decryptedPersonalMessage ?? ''; + + return ( + onNext()} disabled={!isChecked}> + {lang.strings.buttons.next} + + } + > + + + + + + + + + + } + height={120} + value={cardLocation} + tooltip={strings.form.cardLocationField.tooltip} + placeholder={strings.form.cardLocationField.placeholder} + autoFocus + /> + } + height={120} + value={personalMessage} + tooltip={strings.form.personalMessageField.tooltip} + placeholder={strings.form.personalMessageField.placeholder} + mb={0} + /> + + setIsChecked(!isChecked)} + label={strings.form.checkBox.label} + /> + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Success.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Success.tsx new file mode 100644 index 000000000..3c420b190 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Success.tsx @@ -0,0 +1,57 @@ +import { constants } from '@cypherock/cysync-core-constants'; +import { + Button, + Container, + Image, + LangDisplay, + successIcon, + Typography, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { VideoPlayer } from '~/components/VideoPlayer'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Success = () => { + const lang = useAppSelector(selectLanguage); + + const strings = + lang.strings.dialogs.inheritanceEstateRecovery.confirmation.success; + + const { onClose, onRetry, retryIndex } = useInheritanceEstateRecoveryDialog(); + + return ( + onClose()}> + + + } + > + + Success Icon + + + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Terms.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Terms.tsx new file mode 100644 index 000000000..d76692c88 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/Terms.tsx @@ -0,0 +1,65 @@ +import { + Button, + CheckBox, + Container, + Flex, + LangDisplay, + Typography, + ExternalLink, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { constants } from '~/constants'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Terms = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritance.termsOfService; + + const { onNext, isTermsAccepted, setIsTermsAccepted } = + useInheritanceEstateRecoveryDialog(); + + const toNextPage = async () => { + if (isTermsAccepted) onNext(); + }; + + return ( + toNextPage()} + > + + + } + > + + + + + + + + + + setIsTermsAccepted(!isTermsAccepted)} + id="privacy_policy_accepted" + label={strings.checkBoxLabel} + /> + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ViewPin.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ViewPin.tsx new file mode 100644 index 000000000..101c22f37 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/ViewPin.tsx @@ -0,0 +1,100 @@ +import { InheritanceDecryptMessageDeviceEvent } from '@cypherock/app-support-inheritance'; +import { + ArrowRightIcon, + Check, + Container, + Image, + LangDisplay, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + Throbber, + Typography, + verifyPinOnDevice, +} from '@cypherock/cysync-ui'; +import React, { useEffect } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../context'; +import { Layout } from '../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const ViewPin = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.dialogs.inheritanceEstateRecovery.viewPin; + + const { + onNext, + decryptPinIsCompleted, + decryptPinDeviceEvents, + selectedWallet, + } = useInheritanceEstateRecoveryDialog(); + + const getDeviceEventIcon = ( + loadingEvent: InheritanceDecryptMessageDeviceEvent, + completedEvent: InheritanceDecryptMessageDeviceEvent, + ) => { + if (decryptPinDeviceEvents[completedEvent]) return checkIconComponent; + if (decryptPinDeviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.actions.view, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceDecryptMessageDeviceEvent.CARD_TAPPED, + InheritanceDecryptMessageDeviceEvent.PIN_VERIFIED, + ), + }, + ]; + + return actions; + }, []); + + useEffect(() => { + if (decryptPinIsCompleted) { + onNext(); + } + }, [decryptPinIsCompleted]); + + return ( + + verify pin on device + + + {strings.title} + + + + + {selectedWallet?.name} + + + + + {actionsList.map(data => ( + + ))} + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/FetchData.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/FetchData.tsx new file mode 100644 index 000000000..5f3fe2425 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/FetchData.tsx @@ -0,0 +1,25 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../../context'; + +export const FetchData = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.dialogs.inheritancePinRecovery.sync.fetch; + const { onNext, retryIndex, fetchEncryptedData, isEncryptedDataFetched } = + useInheritanceEstateRecoveryDialog(); + + useEffect(() => { + fetchEncryptedData(); + }, [retryIndex]); + + useEffect(() => { + if (isEncryptedDataFetched) { + onNext(); + } + }, [isEncryptedDataFetched]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/FetchRequestId.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/FetchRequestId.tsx new file mode 100644 index 000000000..f85155216 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/FetchRequestId.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; + +import { useInheritanceEstateRecoveryDialog } from '../../context'; + +export const FetchRequestId = () => { + const { onNext, walletAuthStep, walletAuthFetchRequestId, retryIndex } = + useInheritanceEstateRecoveryDialog(); + + useEffect(() => { + walletAuthFetchRequestId(); + }, [retryIndex]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.fetchRequestId) { + onNext(); + } + }, [walletAuthStep]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/ValidateSignature.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/ValidateSignature.tsx new file mode 100644 index 000000000..219504bdd --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/ValidateSignature.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; + +import { useInheritanceEstateRecoveryDialog } from '../../context'; + +export const ValidateSignature = () => { + const { onNext, walletAuthStep, walletAuthValidateSignature, retryIndex } = + useInheritanceEstateRecoveryDialog(); + + useEffect(() => { + walletAuthValidateSignature(); + }, [retryIndex]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.validateSignature) { + onNext(); + } + }, [walletAuthStep]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/VerifyOTP.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/VerifyOTP.tsx new file mode 100644 index 000000000..7052defdd --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/VerifyOTP.tsx @@ -0,0 +1,76 @@ +import React, { useMemo, useRef, useCallback, useEffect } from 'react'; + +import { + OTPInputDialog, + OTPInputDialogRef, +} from '~/dialogs/Inheritance/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../../context'; + +export const VerifyOTP: React.FC = () => { + const lang = useAppSelector(selectLanguage); + const { title } = + lang.strings.dialogs.inheritanceEstateRecovery.wallet.verification; + const { + onPrevious, + onNext, + otpVerificationDetails, + verifyOtp, + isVerifyingOtp, + } = useInheritanceEstateRecoveryDialog(); + + const otpRef = useRef(null); + + const onVerify = useCallback( + async (otp: string) => { + await verifyOtp(otp); + }, + [verifyOtp], + ); + + const onResend = () => { + // DUMMY FUNCTION + }; + + const otpExpireTime = useMemo( + () => otpVerificationDetails?.otpExpiry ?? '', + [otpVerificationDetails?.otpExpiry], + ); + + const otpLength = 6; + + const retriesRemaining = otpVerificationDetails?.retriesRemaining ?? 3; + const email = otpVerificationDetails?.emails ?? ''; + + useEffect(() => { + if (otpRef.current) { + otpRef.current.reset(); + } + }, [otpVerificationDetails?.id]); + + useEffect(() => { + if (!otpVerificationDetails) onNext(); + }, [otpVerificationDetails]); + + if (!otpVerificationDetails) { + return null; + } + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/WalletAuth.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/WalletAuth.tsx new file mode 100644 index 000000000..affa1469f --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/WalletAuth.tsx @@ -0,0 +1,155 @@ +import { InheritanceWalletAuthDeviceEvent } from '@cypherock/app-support-inheritance'; +import { + ArrowRightIcon, + CardTapList, + Check, + Container, + LangDisplay, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + tapAnyCardDeviceAnimation2DVideo, + Throbber, + Typography, + Video, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useState } from 'react'; + +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceEstateRecoveryDialog } from '../../context'; +import { Layout } from '../../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const WalletAuth = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.dialogs.inheritanceEstateRecovery; + + const [cardTapState, setCardTapState] = useState(-1); + + const { + onNext, + selectedWallet, + walletAuthDeviceEvents, + walletAuthStep, + walletAuthStart, + walletAuthAbort, + retryIndex, + clearErrors, + isRegisterationRequired, + } = useInheritanceEstateRecoveryDialog(); + + const getDeviceEventIcon = ( + loadingEvent: InheritanceWalletAuthDeviceEvent, + completedEvent: InheritanceWalletAuthDeviceEvent, + ) => { + if (walletAuthDeviceEvents[completedEvent]) return checkIconComponent; + if (walletAuthDeviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.wallet.actions.confirm, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceWalletAuthDeviceEvent.INIT, + InheritanceWalletAuthDeviceEvent.CONFIRMED, + ), + }, + ]; + + return actions; + }, [strings, walletAuthDeviceEvents, isRegisterationRequired]); + + useEffect(() => { + const eventToState: Record = { + [InheritanceWalletAuthDeviceEvent.CONFIRMED]: 0, + [InheritanceWalletAuthDeviceEvent.CARD_PAIRING_CARD_TAPPED]: 1, + [InheritanceWalletAuthDeviceEvent.WALLET_BASED_CARD_TAPPED]: 2, + }; + + let state: number | undefined; + for (const event in eventToState) { + if (walletAuthDeviceEvents[event] && eventToState[event] !== undefined) { + state = eventToState[event]!; + } + } + + if (state !== undefined) { + setCardTapState(state); + } + }, [walletAuthDeviceEvents]); + + useEffect(() => { + clearErrors(); + walletAuthStart(); + setCardTapState(-1); + + return () => { + walletAuthAbort(); + }; + }, [retryIndex, clearErrors]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.walletAuth) { + onNext(); + } + }, [walletAuthStep]); + + return ( + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/index.tsx new file mode 100644 index 000000000..215337c7a --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/WalletAuth/index.tsx @@ -0,0 +1,5 @@ +export * from './FetchRequestId'; +export * from './WalletAuth'; +export * from './ValidateSignature'; +export * from './VerifyOTP'; +export * from './FetchData'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/index.tsx new file mode 100644 index 000000000..c34503fce --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Dialogs/index.tsx @@ -0,0 +1,7 @@ +export * from './Terms'; +export * from './WalletAuth'; +export * from './Decrypt'; +export * from './ViewPin'; +export * from './Message'; +export * from './Success'; +export * from './ClearDeviceData'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Layout.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Layout.tsx new file mode 100644 index 000000000..eb50005af --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/Layout.tsx @@ -0,0 +1,31 @@ +import { + DialogBox, + DialogBoxBody, + DialogBoxFooter, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +export interface LayoutProps { + children: React.ReactNode; + footerComponent?: React.ReactNode; +} + +export const Layout: React.FC = ({ + children, + footerComponent, +}) => ( + + + {children} + + {footerComponent && ( + + {footerComponent} + + )} + +); + +Layout.defaultProps = { + footerComponent: undefined, +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/context/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/context/index.tsx new file mode 100644 index 000000000..f838a97df --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/context/index.tsx @@ -0,0 +1,291 @@ +import { assert } from '@cypherock/cysync-utils'; +import React, { + Context, + FC, + ReactNode, + createContext, + useCallback, + useContext, + useMemo, + useRef, + useState, +} from 'react'; + +import { openContactSupportDialog } from '~/actions'; +import { useAsync, useMemoReturn } from '~/hooks'; +import { InheritanceUserTypeMap, inheritancePlanService } from '~/services'; +import { useAppDispatch, useAppSelector } from '~/store'; + +import { + InheritanceEstateRecoveryDialogContextInterface, + InheritanceEstateRecoveryDialogProps, +} from './types'; +import { + tabIndicies, + useEstateRecoveryDialogHanlders, +} from './useDialogHander'; + +import { + useDecryptMessage, + useSession, + useWalletAuth, + WalletAuthLoginStep, +} from '../../hooks'; + +export * from './types'; + +export const InheritanceEstateRecoveryDialogContext: Context = + createContext( + {} as InheritanceEstateRecoveryDialogContextInterface, + ); + +export interface InheritanceEstateRecoveryDialogContextProviderProps + extends InheritanceEstateRecoveryDialogProps { + children: ReactNode; +} + +export const InheritanceEstateRecoveryDialogProvider: FC< + InheritanceEstateRecoveryDialogContextProviderProps +> = ({ children, walletId }) => { + const { + currentDialog, + currentTab, + goTo, + isDeviceRequired, + onClose, + onNext, + onPrevious, + tabs, + } = useEstateRecoveryDialogHanlders(); + + const wallets = useAppSelector(state => state.wallet.wallets); + const selectedWallet = useMemo( + () => wallets.find(w => w.__id === walletId), + [walletId, wallets], + ); + + const dispatch = useAppDispatch(); + + const [unhandledError, setUnhandledError] = useState(); + const [retryIndex, setRetryIndex] = useState(0); + const [isTermsAccepted, setIsTermsAccepted] = useState(false); + const encryptedMessageRef = useRef(); + + const onError = useCallback((e?: any) => { + setUnhandledError(e); + }, []); + + const clearErrors = useCallback(() => { + setUnhandledError(undefined); + }, []); + + const walletAuthService = useWalletAuth(onError); + const sessionService = useSession(onError); + const decryptMessageService = useDecryptMessage(onError); + + const authTokenConfig = useMemo( + () => walletAuthService.authTokenConfig, + [walletAuthService], + ); + + const walletAuthFetchRequestId = useCallback(() => { + walletAuthService.fetchRequestId( + walletId, + InheritanceUserTypeMap.nominee, + 'wallet-based', + ); + }, [walletId, walletAuthService.fetchRequestId]); + + const fetchEncryptedDataHandler = useCallback(async () => { + let sessionId = await sessionService.getIsActive(); + + if (!sessionId) { + sessionId = await sessionService.start(); + } + + assert(sessionId, 'sessionId not found'); + assert(authTokenConfig, 'accessToken not found'); + + const result = await inheritancePlanService.recover.recover({ + sessionId, + authTokenConfig, + message: true, + nominee: true, + wallet: true, + }); + + if (result.error) { + throw result.error; + } + + encryptedMessageRef.current = result.result.encryptedMessage; + return true; + }, [sessionService.start, sessionService.getIsActive, authTokenConfig]); + + const [ + fetchEncryptedData, + isFetchingEncryptedData, + isEncryptedDataFetched, + resetFetchEncryptedData, + ] = useAsync(fetchEncryptedDataHandler, onError); + + const startDecryption = useCallback(async () => { + assert(encryptedMessageRef.current, 'encryptedMessage not found'); + decryptMessageService.start(walletId, encryptedMessageRef.current); + }, [walletId, decryptMessageService.start]); + + const resetAll = useCallback(() => { + setRetryIndex(v => v + 1); + setUnhandledError(undefined); + walletAuthService.reset(); + sessionService.reset(); + resetFetchEncryptedData(); + decryptMessageService.reset(); + }, [ + walletAuthService.reset, + sessionService.reset, + resetFetchEncryptedData, + decryptMessageService.reset, + ]); + + const onRetryFuncMap = useMemo< + Record boolean) | undefined> | undefined> + >(() => ({}), []); + + const onRetry = useCallback(() => { + const retryLogic = onRetryFuncMap[currentTab]?.[currentDialog]; + + if (retryLogic) { + setRetryIndex(v => v + 1); + retryLogic(); + } else { + resetAll(); + goTo(0, 0); + } + + setUnhandledError(undefined); + }, [currentTab, currentDialog, onRetryFuncMap, walletAuthService.reset]); + + const onNextActionMapPerDialog = useMemo< + Record boolean) | undefined> | undefined> + >( + () => ({ + [tabIndicies.wallet.tabNumber]: { + [tabIndicies.wallet.dialogs.fetchRequestId]: () => { + if (walletAuthService.currentStep === WalletAuthLoginStep.completed) { + goTo( + tabIndicies.wallet.tabNumber, + tabIndicies.wallet.dialogs.fetchData, + ); + return true; + } + + return false; + }, + }, + }), + [walletAuthService.currentStep], + ); + + const onNextCallback = useCallback(() => { + const action = onNextActionMapPerDialog[currentTab]?.[currentDialog]; + + let doNext = true; + + if (action) { + doNext = !action(); + } + + if (doNext) { + onNext(); + } + }, [onNext, currentTab, currentDialog]); + + const onPreviousActionMapPerDialog = useMemo< + Record boolean) | undefined> | undefined> + >( + () => ({ + [tabIndicies.wallet.tabNumber]: { + [tabIndicies.wallet.dialogs.verifyOtp]: () => { + walletAuthService.reset(); + goTo( + tabIndicies.instructions.tabNumber, + tabIndicies.instructions.dialogs.authClearData, + ); + return true; + }, + }, + }), + [], + ); + + const onPreviousCallback = useCallback(() => { + const action = onPreviousActionMapPerDialog[currentTab]?.[currentDialog]; + + let doPrevious = true; + + if (action) { + doPrevious = !action(); + } + + if (doPrevious) { + onPrevious(); + } + }, [onPrevious, currentTab, currentDialog]); + + const onHelp = () => { + dispatch(openContactSupportDialog()); + }; + + const ctx = useMemoReturn({ + onNext: onNextCallback, + onPrevious: onPreviousCallback, + tabs, + onClose, + onHelp, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + unhandledError, + onRetry, + isTermsAccepted, + setIsTermsAccepted, + clearErrors, + retryIndex, + walletAuthDeviceEvents: walletAuthService.deviceEvents, + walletAuthFetchRequestId, + walletAuthIsFetchingRequestId: walletAuthService.isFetchingRequestId, + walletAuthStart: walletAuthService.startWalletAuth, + walletAuthValidateSignature: walletAuthService.validateSignature, + walletAuthIsValidatingSignature: walletAuthService.isValidatingSignature, + walletAuthStep: walletAuthService.currentStep, + walletAuthAbort: walletAuthService.abortWalletAuth, + onRegister: walletAuthService.registerUser, + isRegisteringUser: walletAuthService.isRegisteringUser, + otpVerificationDetails: walletAuthService.otpVerificationDetails, + verifyOtp: walletAuthService.verifyOtp, + isVerifyingOtp: walletAuthService.isVerifyingOtp, + isRegisterationRequired: walletAuthService.isRegisterationRequired, + fetchEncryptedData, + isFetchingEncryptedData, + isEncryptedDataFetched, + selectedWallet, + decryptPinStart: startDecryption, + decryptPinAbort: decryptMessageService.abort, + decryptPinDeviceEvents: decryptMessageService.deviceEvents, + decryptPinIsCompleted: decryptMessageService.isDecrypted, + decryptedCardLocation: decryptMessageService.cardLocation, + decryptedPersonalMessage: decryptMessageService.personalMessage, + }); + + return ( + + {children} + + ); +}; + +export function useInheritanceEstateRecoveryDialog(): InheritanceEstateRecoveryDialogContextInterface { + return useContext(InheritanceEstateRecoveryDialogContext); +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/context/types.ts b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/context/types.ts new file mode 100644 index 000000000..7ef96269e --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/context/types.ts @@ -0,0 +1,56 @@ +import { IWallet } from '@cypherock/db-interfaces'; + +import { ITabs } from '~/hooks'; + +import { WalletAuthLoginStep, IOtpVerificationDetails } from '../../hooks'; + +export interface IUserDetails { + name: string; + email: string; + alternateEmail: string; +} + +export interface InheritanceEstateRecoveryDialogContextInterface { + tabs: ITabs; + onNext: (tab?: number, dialog?: number) => void; + goTo: (tab: number, dialog?: number) => void; + onPrevious: () => void; + currentTab: number; + currentDialog: number; + isDeviceRequired: boolean; + onClose: () => void; + onHelp: () => void; + userDetails?: IUserDetails; + unhandledError?: any; + onRetry: () => void; + retryIndex: number; + clearErrors: () => void; + isTermsAccepted: boolean; + setIsTermsAccepted: (value: boolean) => void; + walletAuthDeviceEvents: Record; + walletAuthFetchRequestId: () => void; + walletAuthIsFetchingRequestId: boolean; + walletAuthStart: () => void; + walletAuthAbort: () => void; + walletAuthIsValidatingSignature: boolean; + walletAuthValidateSignature: () => Promise; + walletAuthStep: WalletAuthLoginStep; + otpVerificationDetails?: IOtpVerificationDetails; + verifyOtp: (otp: string) => Promise; + isVerifyingOtp: boolean; + isRegisterationRequired: boolean; + fetchEncryptedData: () => Promise; + isFetchingEncryptedData: boolean; + isEncryptedDataFetched: boolean; + selectedWallet?: IWallet; + decryptPinStart: () => void; + decryptPinAbort: () => void; + decryptPinDeviceEvents: Record; + decryptPinIsCompleted: boolean; + decryptedCardLocation?: string; + decryptedPersonalMessage?: string; +} + +export interface InheritanceEstateRecoveryDialogProps { + walletId: string; +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/context/useDialogHander.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/context/useDialogHander.tsx new file mode 100644 index 000000000..b3aa64631 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/context/useDialogHander.tsx @@ -0,0 +1,170 @@ +import React, { useCallback, useMemo } from 'react'; + +import { ITabs, useTabsAndDialogs } from '~/hooks'; +import { + useAppDispatch, + useAppSelector, + selectLanguage, + closeDialog, +} from '~/store'; + +import { + WalletAuth, + VerifyOTP, + Terms, + Success, + AuthenticateClearData, + ClearData, + ConfirmClearData, + DecryptMessage, + FetchData, + Message, + Settings, + ViewPin, + FetchRequestId, + ValidateSignature, +} from '../Dialogs'; + +export const tabIndicies = { + terms: { + tabNumber: 0, + dialogs: { + terms: 0, + }, + }, + instructions: { + tabNumber: 1, + dialogs: { + settings: 0, + clearData: 1, + confirmClearData: 2, + authClearData: 3, + }, + }, + wallet: { + tabNumber: 2, + dialogs: { + fetchRequestId: 0, + walletAuth: 1, + validateSignature: 2, + verifyOtp: 3, + fetchData: 4, + }, + }, + decrypt: { + tabNumber: 3, + dialogs: { + decrypt: 0, + }, + }, + viewPin: { + tabNumber: 4, + dialogs: { + viewPin: 0, + }, + }, + message: { + tabNumber: 5, + dialogs: { + message: 0, + }, + }, + success: { + tabNumber: 6, + dialogs: { + success: 0, + }, + }, +}; + +export const useEstateRecoveryDialogHanlders = () => { + const dispatch = useAppDispatch(); + const lang = useAppSelector(selectLanguage); + + const deviceRequiredDialogsMap: Record = + useMemo( + () => ({ + [tabIndicies.wallet.tabNumber]: [ + tabIndicies.wallet.dialogs.walletAuth, + tabIndicies.wallet.dialogs.fetchData, + ], + [tabIndicies.decrypt.tabNumber]: [tabIndicies.decrypt.dialogs.decrypt], + [tabIndicies.viewPin.tabNumber]: [tabIndicies.viewPin.dialogs.viewPin], + }), + [], + ); + + const tabs: ITabs = useMemo( + () => [ + { + name: lang.strings.inheritance.termsOfService.title, + dialogs: [], + }, + { + name: lang.strings.dialogs.inheritanceEstateRecovery.instructions.name, + dialogs: [ + , + , + , + , + ], + }, + { + name: lang.strings.dialogs.inheritanceEstateRecovery.wallet.name, + dialogs: [ + , + , + , + , + , + ], + }, + { + name: lang.strings.dialogs.inheritanceEstateRecovery.decryption.name, + dialogs: [], + }, + { + name: lang.strings.dialogs.inheritanceEstateRecovery.viewPin.name, + dialogs: [], + }, + { + name: lang.strings.dialogs.inheritanceEstateRecovery.viewMessage.name, + dialogs: [], + }, + { + name: lang.strings.dialogs.inheritanceEstateRecovery.confirmation.name, + dialogs: [], + dontShowOnMilestone: true, + }, + ], + [], + ); + + const { + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + } = useTabsAndDialogs({ + deviceRequiredDialogsMap, + tabs, + dialogName: 'inheritanceEstateRecovery', + }); + + const onClose = useCallback(() => { + dispatch(closeDialog('inheritanceEstateRecovery')); + }, []); + + return { + onClose, + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + tabs, + }; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/index.tsx new file mode 100644 index 000000000..bb9de5921 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/EstateRecovery/index.tsx @@ -0,0 +1,118 @@ +import { + BlurOverlay, + CloseButton, + DialogBox, + DialogBoxBackgroundBar, + DialogBoxBody, + HelpButton, + MilestoneAside, + WalletDialogMainContainer, +} from '@cypherock/cysync-ui'; +import React, { FC } from 'react'; + +import { + ErrorHandlerDialog, + WithConnectedDevice, + WithConnectedDeviceProps, +} from '~/components'; +import { useAppSelector, selectLanguage } from '~/store'; + +import { + InheritanceEstateRecoveryDialogProps, + InheritanceEstateRecoveryDialogProvider, + useInheritanceEstateRecoveryDialog, +} from './context'; + +export { type InheritanceEstateRecoveryDialogProps } from './context'; + +const DeviceConnectionWrapper: React.FC<{ + isDeviceRequired: boolean; + children: React.ReactNode; + withConnectedProps?: WithConnectedDeviceProps; +}> = ({ isDeviceRequired, withConnectedProps, children }) => { + if (isDeviceRequired) + return ( + + {children} + + ); + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; +}; + +DeviceConnectionWrapper.defaultProps = { + withConnectedProps: {}, +}; + +const InheritanceEstateRecovery: FC = () => { + const { + tabs, + currentTab, + currentDialog, + unhandledError, + onClose, + onHelp, + isDeviceRequired, + onRetry, + } = useInheritanceEstateRecoveryDialog(); + + const lang = useAppSelector(selectLanguage); + + return ( + + + <> + !t.dontShowOnMilestone) + .map(t => t.name)} + activeTab={currentTab} + heading={lang.strings.dialogs.inheritanceEstateRecovery.title} + /> + + + + + {tabs[currentTab]?.dialogs[currentDialog]} + + + + onClose()} />} + leftComponent={ + + } + position="top" + useLightPadding + /> + + + + + ); +}; + +export const InheritanceEstateRecoveryDialog: React.FC< + InheritanceEstateRecoveryDialogProps +> = props => ( + + + +); diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Checkout.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Checkout.tsx new file mode 100644 index 000000000..76d20f3a9 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Checkout.tsx @@ -0,0 +1,94 @@ +import { + Button, + Container, + LangDisplay, + Payment, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useState } from 'react'; + +import { constants } from '~/constants'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Checkout = () => { + const [tempCoupon, setCoupon] = useState(''); + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceGoldPlanPurchase.checkout; + const [isValidCoupon, setIsValidCoupon] = useState(false); + const [isPlanActivated, setIsPlanActivated] = useState(false); + + const { + onNext, + applyCoupon, + removeCoupon, + isApplyingCoupon, + applyingCouponError, + couponDuration, + isActivatingCoupon, + activateCoupon, + } = useInheritanceGoldPlanPurchaseDialog(); + + const onApply = async () => { + const isValid = await applyCoupon(tempCoupon); + setIsValidCoupon(isValid); + }; + + const onActivate = async () => { + const isActivated = await activateCoupon(); + setIsPlanActivated(isActivated); + }; + + const onDelete = () => { + setCoupon(''); + removeCoupon(); + setIsValidCoupon(false); + setIsPlanActivated(false); + }; + + const couponText = tempCoupon; + + useEffect(() => { + if (isPlanActivated) { + onNext(); + } + }, [isPlanActivated, onNext]); + + return ( + + {lang.strings.buttons.confirm} + + } + > + + + + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Email/UserDetails.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Email/UserDetails.tsx new file mode 100644 index 000000000..f2accc7ec --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Email/UserDetails.tsx @@ -0,0 +1,102 @@ +import { Button, LangDisplay } from '@cypherock/cysync-ui'; +import React, { useEffect, useMemo, useState } from 'react'; +import { UserDetailsForm } from '~/dialogs/Inheritance/components'; + +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +export const UserDetails = () => { + const lang = useAppSelector(selectLanguage); + + const goldPlanStrings = + lang.strings.inheritanceGoldPlanPurchase.email.userDetails; + + const { + registerUser, + onPrevious, + onNext, + isRegisteringUser, + walletAuthStep, + userDetailPrefillData, + } = useInheritanceGoldPlanPurchaseDialog(); + + const [name, setName] = useState(userDetailPrefillData.name); + const [email, setEmail] = useState(userDetailPrefillData.email); + const [alternateEmail, setAlternateEmail] = useState( + userDetailPrefillData.alternateEmail, + ); + + const formId = 'inheritance-gold-plan-user-details'; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (isRegisteringUser) return; + + registerUser({ + name, + email, + alternateEmail, + }); + }; + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.userDetails) { + onNext(); + } + }, []); + + const isSameEmail = useMemo( + () => Boolean(email && email === alternateEmail), + [email, alternateEmail], + ); + + const [hasErrors, setHasErrors] = useState(false); + const isFormIncomplete = useMemo( + () => !name || !email || !alternateEmail || hasErrors, + [hasErrors, name, email, alternateEmail], + ); + + return ( + + + + + } + > + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Email/VerifyOTP.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Email/VerifyOTP.tsx new file mode 100644 index 000000000..10e81e2df --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Email/VerifyOTP.tsx @@ -0,0 +1,98 @@ +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; + +import { + OTPInputDialog, + OTPInputDialogRef, +} from '~/dialogs/Inheritance/components'; +import { OtpVerificationConcern } from '~/dialogs/Inheritance/hooks'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { tabIndicies } from '../../context/useDialogHandler'; + +export const VerifyOTP: React.FC = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritance.dialog.verifyOTP; + + const { + onPrevious, + onNext, + otpVerificationDetails, + verifyOtp, + isVerifyingOtp, + isOnSummaryPage, + goTo, + onClose, + } = useInheritanceGoldPlanPurchaseDialog(); + + const title = useMemo(() => { + const map: Record = { + [OtpVerificationConcern.primary]: strings.primaryEmailOTP.title, + [OtpVerificationConcern.alternate]: strings.alternateEmailOTP.title, + [OtpVerificationConcern.login]: + lang.strings.dialogs.inheritancePlanLogin.verifyEmail.title, + }; + + if (!otpVerificationDetails) return ''; + + return map[otpVerificationDetails.concern]; + }, [otpVerificationDetails?.concern, lang]); + + const otpRef = useRef(null); + + const onVerify = useCallback( + async (otp: string) => { + await verifyOtp(otp); + }, + [verifyOtp], + ); + + const onResend = () => { + // DUMMY FUNCTION + }; + + const otpExpireTime = useMemo( + () => otpVerificationDetails?.otpExpiry ?? '', + [otpVerificationDetails?.otpExpiry], + ); + + const otpLength = 6; + + const retriesRemaining = otpVerificationDetails?.retriesRemaining ?? 3; + const email = otpVerificationDetails?.emails ?? ''; + + useEffect(() => { + if (otpRef.current) { + otpRef.current.reset(); + } + }, [otpVerificationDetails?.id]); + + useEffect(() => { + if (!otpVerificationDetails) { + if (isOnSummaryPage) goTo(tabIndicies.summary.tabNumber); + else onNext(); + } + }, [otpVerificationDetails]); + + if (!otpVerificationDetails) { + return null; + } + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Email/index.ts b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Email/index.ts new file mode 100644 index 000000000..549cfb854 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Email/index.ts @@ -0,0 +1,2 @@ +export * from './UserDetails'; +export * from './VerifyOTP'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/Device.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/Device.tsx new file mode 100644 index 000000000..69e7a44c5 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/Device.tsx @@ -0,0 +1,163 @@ +import { InheritanceEncryptMessageDeviceEvent } from '@cypherock/app-support-inheritance'; +import { + ArrowRightIcon, + CardTapList, + Check, + Container, + LangDisplay, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + QuestionMarkButton, + tapAnyCardDeviceAnimation2DVideo, + Throbber, + Tooltip, + Typography, + Video, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useState } from 'react'; + +import { LoaderDialog } from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const DeviceEncryption = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritanceGoldPlanPurchase.encryption.device; + const [cardTapState, setCardTapState] = useState(-1); + + const { + onNext, + encryptPinDeviceEvents, + encryptPinAbort, + encryptPinStart, + clearErrors, + retryIndex, + encryptPinIsCompleted, + isEstablishingSession, + } = useInheritanceGoldPlanPurchaseDialog(); + + const getDeviceEventIcon = ( + loadingEvent: InheritanceEncryptMessageDeviceEvent, + completedEvent: InheritanceEncryptMessageDeviceEvent, + ) => { + if (encryptPinDeviceEvents[completedEvent]) return checkIconComponent; + if (encryptPinDeviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.actions.confirmOnDevice, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceEncryptMessageDeviceEvent.INIT, + InheritanceEncryptMessageDeviceEvent.CONFIRMED, + ), + }, + ]; + + return actions; + }, [encryptPinDeviceEvents]); + + useEffect(() => { + const eventToState: Record = { + [InheritanceEncryptMessageDeviceEvent.CONFIRMED]: 0, + [InheritanceEncryptMessageDeviceEvent.PIN_CARD_TAPPED]: 1, + [InheritanceEncryptMessageDeviceEvent.MESSAGE_CARD_TAPPED]: 2, + }; + + let state: number | undefined; + for (const event in eventToState) { + if (encryptPinDeviceEvents[event] && eventToState[event] !== undefined) { + state = eventToState[event]!; + } + } + + if (state !== undefined) { + setCardTapState(state); + } + }, [encryptPinDeviceEvents]); + + useEffect(() => { + clearErrors(); + encryptPinStart(); + setCardTapState(-1); + + return () => { + encryptPinAbort(); + }; + }, [retryIndex, clearErrors]); + + useEffect(() => { + if (encryptPinIsCompleted) { + onNext(); + } + }, [encryptPinIsCompleted]); + + if (isEstablishingSession) { + return ; + } + + return ( + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/Loader.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/Loader.tsx new file mode 100644 index 000000000..46cb6d8f1 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/Loader.tsx @@ -0,0 +1,34 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { useCallbackAfterCountdown } from '~/hooks'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; + +export const EncryptionLoader: React.FC = () => { + const lang = useAppSelector(selectLanguage); + const { onNext, setupPlan, isSetupPlanCompleted, retryIndex } = + useInheritanceGoldPlanPurchaseDialog(); + + const strings = lang.strings.inheritanceGoldPlanPurchase.encryption.loading; + + const onNextCallback = useCallbackAfterCountdown(onNext, 3000); + + useEffect(() => { + onNextCallback.setStartTime(Date.now()); + setupPlan(); + }, [retryIndex]); + + useEffect(() => { + if (isSetupPlanCompleted) { + onNextCallback.start(); + } + + return () => { + onNextCallback.stop(); + }; + }, [isSetupPlanCompleted]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/Success.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/Success.tsx new file mode 100644 index 000000000..ab2f025b8 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/Success.tsx @@ -0,0 +1,27 @@ +import { SuccessDialog } from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; + +export const EncryptionSuccess: React.FC = () => { + const { onClose, onNext } = useInheritanceGoldPlanPurchaseDialog(); + + const lang = useAppSelector(selectLanguage); + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/index.tsx new file mode 100644 index 000000000..95ba8a4ea --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Encryption/index.tsx @@ -0,0 +1,3 @@ +export * from './Device'; +export * from './Loader'; +export * from './Success'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Ensure.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Ensure.tsx new file mode 100644 index 000000000..4de317431 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Ensure.tsx @@ -0,0 +1,64 @@ +import { + Button, + Container, + LangDisplay, + Typography, + ListContainer, + ListItem, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Ensure = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritanceGoldPlanPurchase; + + const { onNext, onPrevious } = useInheritanceGoldPlanPurchaseDialog(); + + return ( + + + + + } + > + + + + + + {strings.instructions.ensure.instructions.map((listItem, index) => ( + + + + + + ))} + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Greeting.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Greeting.tsx new file mode 100644 index 000000000..fa1813ab4 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Greeting.tsx @@ -0,0 +1,27 @@ +import { SuccessDialog } from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../context'; + +export const Greeting = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceGoldPlanPurchase.checkout.greeting; + + const { onClose } = useInheritanceGoldPlanPurchaseDialog(); + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Instructions.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Instructions.tsx new file mode 100644 index 000000000..967f0b8dc --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Instructions.tsx @@ -0,0 +1,55 @@ +import { constants } from '@cypherock/cysync-core-constants'; +import { + Button, + Container, + LangDisplay, + Typography, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { VideoPlayer } from '~/components/VideoPlayer'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Instructions = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceGoldPlanPurchase; + + const { onNext, onPrevious, onRetry, retryIndex } = + useInheritanceGoldPlanPurchaseDialog(); + + return ( + + + + + } + > + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/ExecutorMessage.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/ExecutorMessage.tsx new file mode 100644 index 000000000..8ec48ea89 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/ExecutorMessage.tsx @@ -0,0 +1,86 @@ +import { + Flex, + Typography, + LangDisplay, + TextAreaInput, + Button, + QuestionMarkButton, + Tooltip, +} from '@cypherock/cysync-ui'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { selectLanguage } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +export const ExecutorMessage = () => { + const { + onPrevious, + executorMessage, + setExecutorMessage, + onExecutorMessageSubmit, + isSubmittingExecutorDetails, + isOnSummaryPage, + } = useInheritanceGoldPlanPurchaseDialog(); + const lang = useSelector(selectLanguage); + const strings = lang.strings.inheritanceGoldPlanPurchase.message; + const { form } = strings.executor; + return ( + + + + + } + > + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/ExecutorMessageTutorial.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/ExecutorMessageTutorial.tsx new file mode 100644 index 000000000..fb9b3d6ed --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/ExecutorMessageTutorial.tsx @@ -0,0 +1,59 @@ +import { constants } from '@cypherock/cysync-core-constants'; +import { + Button, + Container, + LangDisplay, + Typography, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { VideoPlayer } from '~/components/VideoPlayer'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +export const ExecutorMessageTutorial = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceGoldPlanPurchase.message; + + const { onNext, onPrevious, onRetry, retryIndex } = + useInheritanceGoldPlanPurchaseDialog(); + + return ( + + + + + } + > + + + + + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/ExecutorReminderTime.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/ExecutorReminderTime.tsx new file mode 100644 index 000000000..33297ed2c --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/ExecutorReminderTime.tsx @@ -0,0 +1,94 @@ +import { + Flex, + Typography, + LangDisplay, + InputLabel, + Button, + Container, + Reminder, + Accordion, +} from '@cypherock/cysync-ui'; +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { selectLanguage } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +export const ExecutorReminderSetup = () => { + const { + onPrevious, + reminderPeriod, + setReminderPeriod, + isSubmittingReminderDetails, + onReminderDetailsSubmit, + isOnSummaryPage, + } = useInheritanceGoldPlanPurchaseDialog(); + const lang = useSelector(selectLanguage); + const strings = + lang.strings.dialogs.inheritanceEditReminderTime.reminderSetup; + const { form } = strings; + + return ( + + + + + } + > + + + + + + + + + {strings.form.reminderField.label} + + setReminderPeriod('monthly')} + /> + setReminderPeriod('quarterly')} + /> + setReminderPeriod('half-yearly')} + /> + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/NomineePrivateMessage.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/NomineePrivateMessage.tsx new file mode 100644 index 000000000..f43f361ee --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/NomineePrivateMessage.tsx @@ -0,0 +1,155 @@ +import { + Button, + Container, + Flex, + LangDisplay, + MessageBox, + QuestionMarkButton, + TextAreaInput, + Tooltip, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useMemo, useState } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { tabIndicies } from '../../context/useDialogHandler'; +import { Layout } from '../../Layout'; +import { validateInputLanguage } from '~/utils'; +import { debounce } from 'lodash'; + +export const NomineePrivateMessageInput = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceGoldPlanPurchase.message; + + const { + onPrevious, + onNext, + cardLocation, + setCardLocation, + personalMessage, + setPersonalMessage, + haveExecutor, + goTo, + isOnSummaryPage, + } = useInheritanceGoldPlanPurchaseDialog(); + + const [cardLocationError, setCardLocationError] = useState(''); + const [personalMessageError, setPersonalMessageError] = useState(''); + + const isNextDisabled = useMemo( + () => + !personalMessage.trim() || + !cardLocation.trim() || + !!cardLocationError || + !!personalMessageError, + [personalMessage, cardLocation, cardLocationError, personalMessageError], + ); + + const validateFields = debounce(() => { + const cardLocationValidation = validateInputLanguage(cardLocation, lang); + const personalMessageValidation = validateInputLanguage( + personalMessage, + lang, + ); + + if (!cardLocationValidation.success) { + setCardLocationError(cardLocationValidation.error.errors[0].message); + } else { + setCardLocationError(''); + } + + if (!personalMessageValidation.success) { + setPersonalMessageError( + personalMessageValidation.error.errors[0].message, + ); + } else { + setPersonalMessageError(''); + } + }, 300); + + useEffect(() => { + validateFields(); + + return () => { + validateFields.cancel(); + }; + }, [cardLocation, personalMessage]); + + return ( + + + + + } + > + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/index.tsx new file mode 100644 index 000000000..441b86a65 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Message/index.tsx @@ -0,0 +1,4 @@ +export * from './ExecutorMessage'; +export * from './ExecutorMessageTutorial'; +export * from './NomineePrivateMessage'; +export * from './ExecutorReminderTime'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/ConfirmNomineeVerification.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/ConfirmNomineeVerification.tsx new file mode 100644 index 000000000..33c174aed --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/ConfirmNomineeVerification.tsx @@ -0,0 +1,76 @@ +import { + Container, + Typography, + LangDisplay, + MessageBox, + Button, +} from '@cypherock/cysync-ui'; +import React, { useState } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +export const ConfirmNomineeVerification: React.FC<{ index: number }> = ({ + index, +}) => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceGoldPlanPurchase.nomineeAndExecutor; + const { onNomineeDetailsSubmit, isSubmittingNomineeDetails } = + useInheritanceGoldPlanPurchaseDialog(); + + const [isYesButtonLoading, setIsYesButtonLoading] = useState(false); + const [isNoButtonLoading, setIsNoButtonLoading] = useState(false); + + return ( + + + + + } + > + + + + + + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/ExecutorDetails.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/ExecutorDetails.tsx new file mode 100644 index 000000000..ffb697352 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/ExecutorDetails.tsx @@ -0,0 +1,161 @@ +import { + Button, + Container, + Flex, + LangDisplay, + QuestionMarkButton, + RadioButton, + Tooltip, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useMemo, useState } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { UserDetailsForm } from '../../../components'; +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +export const ExecutorDetails = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritanceGoldPlanPurchase.nomineeAndExecutor; + const { form } = lang.strings.inheritanceGoldPlanPurchase.email.userDetails; + + const { + onExecutorDetailsSubmit, + onPrevious, + isSubmittingExecutorDetails, + nomineeCount, + isOnSummaryPage, + executorDetails, + executorNomineeIndex, + } = useInheritanceGoldPlanPurchaseDialog(); + + const [name, setName] = useState(executorDetails?.name ?? ''); + const [email, setEmail] = useState(executorDetails?.email ?? ''); + const [alternateEmail, setAlternateEmail] = useState( + executorDetails?.alternateEmail ?? '', + ); + const [selectedNominee, setSelectedNominee] = useState( + executorNomineeIndex ?? 0, + ); + + const formId = 'inheritance-gold-plan-user-details'; + + const onSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (isSubmittingExecutorDetails) return; + + onExecutorDetailsSubmit( + { + name, + email, + alternateEmail, + }, + selectedNominee, + ); + }; + + const [hasErrors, setHasErrors] = useState(false); + const isSameEmail = useMemo( + () => Boolean(email && email === alternateEmail), + [email, alternateEmail], + ); + const isFormIncomplete = useMemo( + () => !name || !email || hasErrors, + [hasErrors, name, email], + ); + + return ( + + + + + } + > + + + + + + {strings.executor.executorDetails.radio.label} + + + + + + + {Array(nomineeCount) + .fill(0) + .map((_, index) => ( + setSelectedNominee(index)} + > + + + {strings.executor.executorDetails.radio.options + .labelPrefix + (index + 1).toString()} + + + ))} + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/Nominee.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/Nominee.tsx new file mode 100644 index 000000000..1d28a636b --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/Nominee.tsx @@ -0,0 +1,91 @@ +import { + Button, + Container, + Flex, + LangDisplay, + OneInMany, + QuestionMarkButton, + Tooltip, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; + +import { selectLanguage } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +export const Nominee = () => { + const lang = useSelector(selectLanguage); + const { + onPrevious, + onNext, + nomineeCount, + setNomineeCount, + fetchExistingDetailsFromServer, + } = useInheritanceGoldPlanPurchaseDialog(); + + const strings = + lang.strings.inheritanceGoldPlanPurchase.nomineeAndExecutor.select; + + useEffect(() => { + fetchExistingDetailsFromServer(); + }, []); + + return ( + + + + + } + > + + + + + + + + + + + + + + + setNomineeCount(1)} + /> + setNomineeCount(2)} + /> + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/NomineeDetails.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/NomineeDetails.tsx new file mode 100644 index 000000000..904cc708e --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/NomineeDetails.tsx @@ -0,0 +1,117 @@ +import { Button, LangDisplay } from '@cypherock/cysync-ui'; +import React, { useMemo, useState } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { UserDetailsForm } from '../../../components'; +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { tabIndicies } from '../../context/useDialogHandler'; +import { Layout } from '../../Layout'; + +export const NomineeDetails: React.FC<{ index: number }> = ({ index }) => { + const lang = useAppSelector(selectLanguage); + + const { form } = lang.strings.inheritanceGoldPlanPurchase.email.userDetails; + const strings = + lang.strings.inheritanceGoldPlanPurchase.nomineeAndExecutor.nomineeDetails[ + index === 0 ? 'first' : 'second' + ]; + + const { + onNext, + updateNomineeDetails, + goTo, + onPrevious, + isOnSummaryPage, + nomineeDetails, + } = useInheritanceGoldPlanPurchaseDialog(); + + const details = useMemo(() => nomineeDetails[index], [nomineeDetails]); + + const [name, setName] = useState(details?.name ?? ''); + const [email, setEmail] = useState(details?.email ?? ''); + const [alternateEmail, setAlternateEmail] = useState( + details?.alternateEmail ?? '', + ); + + const formId = 'inheritance-gold-plan-user-details'; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + updateNomineeDetails( + { + name, + email, + alternateEmail, + }, + index, + ); + onNext(); + }; + + const [hasErrors, setHasErrors] = useState(false); + const isSameEmail = useMemo( + () => Boolean(email && email === alternateEmail), + [email, alternateEmail], + ); + const isFormIncomplete = useMemo( + () => !name || !email || hasErrors, + [hasErrors, name, email], + ); + + return ( + + + + + } + > + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/SelectExecutor.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/SelectExecutor.tsx new file mode 100644 index 000000000..41b6fccf0 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/SelectExecutor.tsx @@ -0,0 +1,84 @@ +import { + Container, + Typography, + LangDisplay, + OneInMany, + Button, + Flex, + QuestionMarkButton, + Tooltip, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { tabIndicies } from '../../context/useDialogHandler'; +import { Layout } from '../../Layout'; + +export const SelectExecutor = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceGoldPlanPurchase.nomineeAndExecutor; + const { goTo, haveExecutor, setHaveExecutor, onExecutorSelected } = + useInheritanceGoldPlanPurchaseDialog(); + + return ( + + + + + } + > + + + + + + + + + + + + + + + + setHaveExecutor(true)} + /> + setHaveExecutor(false)} + /> + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/VerifyNomineeOtp.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/VerifyNomineeOtp.tsx new file mode 100644 index 000000000..a82ccec4a --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/VerifyNomineeOtp.tsx @@ -0,0 +1,97 @@ +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; + +import { + OTPInputDialog, + OTPInputDialogRef, +} from '~/dialogs/Inheritance/components'; +import { OtpVerificationConcern } from '~/dialogs/Inheritance/hooks'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { tabIndicies } from '../../context/useDialogHandler'; + +export const VerifyNomineeOtp: React.FC = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritance.dialog.verifyOTP; + + const { + onPrevious, + onNext, + nomineeOtpSubmit, + nomineeOtpVerificationDetails, + isSubmittingNomineeDetails, + isOnSummaryPage, + goTo, + onClose, + } = useInheritanceGoldPlanPurchaseDialog(); + + const title = useMemo(() => { + const map: Record = { + [OtpVerificationConcern.primary]: strings.primaryEmailOTP.title, + [OtpVerificationConcern.alternate]: strings.alternateEmailOTP.title, + [OtpVerificationConcern.login]: lang.strings.otp.title, + }; + + if (!nomineeOtpVerificationDetails) return ''; + + return map[nomineeOtpVerificationDetails.concern]; + }, [nomineeOtpVerificationDetails?.concern, lang]); + + const otpRef = useRef(null); + + const onVerify = useCallback( + async (otp: string) => { + await nomineeOtpSubmit(otp); + }, + [nomineeOtpSubmit], + ); + + const onResend = () => { + // DUMMY FUNCTION + }; + + const otpExpireTime = useMemo( + () => nomineeOtpVerificationDetails?.otpExpiry ?? '', + [nomineeOtpVerificationDetails?.otpExpiry], + ); + + const otpLength = 6; + + const retriesRemaining = nomineeOtpVerificationDetails?.retriesRemaining ?? 3; + const email = nomineeOtpVerificationDetails?.emails ?? ''; + + useEffect(() => { + if (otpRef.current) { + otpRef.current.reset(); + } + }, [nomineeOtpVerificationDetails?.id]); + + useEffect(() => { + if (!nomineeOtpVerificationDetails) { + if (isOnSummaryPage) goTo(tabIndicies.summary.tabNumber); + else onNext(); + } + }, [nomineeOtpVerificationDetails]); + + if (!nomineeOtpVerificationDetails) { + return null; + } + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/index.tsx new file mode 100644 index 000000000..e8ff773c4 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/NomineeAndExecutor/index.tsx @@ -0,0 +1,6 @@ +export * from './Nominee'; +export * from './NomineeDetails'; +export * from './ConfirmNomineeVerification'; +export * from './VerifyNomineeOtp'; +export * from './ExecutorDetails'; +export * from './SelectExecutor'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/SelectWallet.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/SelectWallet.tsx new file mode 100644 index 000000000..02d138106 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/SelectWallet.tsx @@ -0,0 +1,142 @@ +import { + Button, + Container, + LangDisplay, + ManyInMany, + ScrollableContainer, + Tooltip, + TooltipPlacement, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useRef, useState } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../context'; +import { Layout } from '../Layout'; + +export const SelectWallet = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritanceGoldPlanPurchase; + + const { onNext, onPrevious, allWallets, selectedWallet, setSelectedWallet } = + useInheritanceGoldPlanPurchaseDialog(); + + const [tooltipPlacements, setTooltipPlacements] = useState< + Record + >({}); + + const observerRef = useRef(); + + const handleObserver = (entries: IntersectionObserverEntry[]) => { + entries.forEach((entry: IntersectionObserverEntry) => { + const walletId = entry.target.getAttribute('data-wallet-id'); + if (walletId) { + let placement: TooltipPlacement; + if (entry.isIntersecting) placement = 'bottom'; + else if ( + entry.rootBounds && + entry.boundingClientRect.bottom > entry.rootBounds.bottom + ) + placement = 'top'; + else placement = 'bottom'; + setTooltipPlacements(prev => ({ ...prev, [walletId]: placement })); + } + }); + }; + + useEffect(() => { + observerRef.current = new IntersectionObserver(handleObserver, { + root: document.querySelector('.scrollable-container'), + threshold: 0.95, + }); + const observedElements = document.querySelectorAll('.wallet-card'); + + observedElements.forEach(el => { + observerRef.current?.observe(el); + }); + + return () => { + observedElements.forEach(el => { + observerRef.current?.unobserve(el); + }); + }; + }, [allWallets]); + + return ( + + + + + } + > + + + + + + + + + + + {allWallets.map(wallet => { + const isDisabled = Boolean(wallet.isDeleted) || !wallet.hasPin; + const walletId = wallet.__id ?? ''; + const tooltipPlacement = tooltipPlacements[walletId] || 'bottom'; + if (isDisabled) { + return ( + +
+ setSelectedWallet(wallet)} + $width={340} + $height={128} + /> +
+
+ ); + } + return ( + { + setSelectedWallet(wallet); + }} + $width={340} + $height={128} + /> + ); + })} +
+
+
+ ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Summary.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Summary.tsx new file mode 100644 index 000000000..d1b7d9694 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Summary.tsx @@ -0,0 +1,245 @@ +import { + Button, + ClockIcon, + Container, + DetailsCard, + EditButton, + EmailIconSmall, + LangDisplay, + parseLangTemplate, + ScrollableContainer, + svgGradients, + Typography, + UserIcon, + WalletIconRounded, +} from '@cypherock/cysync-ui'; +import React, { useCallback, useEffect, useMemo } from 'react'; + +import { ReminderPeriod } from '~/services/inheritance/login/schema'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../context'; +import { tabIndicies } from '../context/useDialogHandler'; +import { Layout } from '../Layout'; + +const goldWalletIcon = ( + +); + +const reminderValueMap: Record = { + monthly: 1, + quarterly: 3, + 'half-yearly': 6, + yearly: 12, +}; + +export const Summary = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceGoldPlanPurchase.summary; + + const { + onNext, + personalMessage, + cardLocation, + nomineeDetails, + userDetails, + selectedWallet, + reminderPeriod, + executorMessage, + executorDetails, + setIsOnSummaryPage, + goTo, + executorNomineeIndex, + } = useInheritanceGoldPlanPurchaseDialog(); + + const reminderValue = useMemo( + () => reminderValueMap[reminderPeriod], + [reminderPeriod], + ); + + useEffect(() => { + setIsOnSummaryPage(true); + }, []); + + const editButtonGoto = useCallback( + (tab: number, dialog?: number) => ( + goTo(tab, dialog)} + /> + ), + [lang, goTo], + ); + + return ( + { + setIsOnSummaryPage(false); + onNext(); + }} + > + + + } + > + + + {strings.title} + + + {strings.subtitle} + + + + + + + {Object.values(nomineeDetails).map((details, index) => ( + + ))} + + + {executorDetails && ( + + )} + + {executorMessage && ( + + )} + + + ); +}; + +export default Summary; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Terms.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Terms.tsx new file mode 100644 index 000000000..896af9a98 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/Terms.tsx @@ -0,0 +1,65 @@ +import { + Button, + CheckBox, + Container, + Flex, + LangDisplay, + Typography, + ExternalLink, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { constants } from '~/constants'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Terms = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritance.termsOfService; + + const { onNext, isTermsAccepted, setIsTermsAccepted } = + useInheritanceGoldPlanPurchaseDialog(); + + const toNextPage = async () => { + if (isTermsAccepted) onNext(); + }; + + return ( + toNextPage()} + > + + + } + > + + + + + + + + + + setIsTermsAccepted(!isTermsAccepted)} + id="privacy_policy_accepted" + label={strings.checkBoxLabel} + /> + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/FetchRequestId.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/FetchRequestId.tsx new file mode 100644 index 000000000..4cca03afc --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/FetchRequestId.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; + +export const FetchRequestId = () => { + const { onNext, walletAuthStep, walletAuthFetchRequestId, retryIndex } = + useInheritanceGoldPlanPurchaseDialog(); + + useEffect(() => { + walletAuthFetchRequestId(); + }, [retryIndex]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.fetchRequestId) { + onNext(); + } + }, [walletAuthStep]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/ValidateSignature.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/ValidateSignature.tsx new file mode 100644 index 000000000..5c463d9b5 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/ValidateSignature.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; + +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; + +export const ValidateSignature = () => { + const { onNext, walletAuthStep, walletAuthValidateSignature, retryIndex } = + useInheritanceGoldPlanPurchaseDialog(); + + useEffect(() => { + walletAuthValidateSignature(); + }, [retryIndex]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.validateSignature) { + onNext(); + } + }, [walletAuthStep]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/WalletAuth.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/WalletAuth.tsx new file mode 100644 index 000000000..79af069ba --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/WalletAuth.tsx @@ -0,0 +1,176 @@ +import { InheritanceWalletAuthDeviceEvent } from '@cypherock/app-support-inheritance'; +import { + ArrowRightIcon, + CardTapList, + Check, + Container, + LangDisplay, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + tapAnyCardDeviceAnimation2DVideo, + Throbber, + Typography, + Video, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useState } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { WalletAuthLoginStep } from '../../../hooks'; +import { useInheritanceGoldPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const WalletAuth = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritanceGoldPlanPurchase; + const [cardTapState, setCardTapState] = useState(-1); + + const { + onNext, + selectedWallet, + walletAuthDeviceEvents, + walletAuthStep, + walletAuthStart, + walletAuthAbort, + retryIndex, + clearErrors, + isRegisterationRequired, + } = useInheritanceGoldPlanPurchaseDialog(); + + const getDeviceEventIcon = ( + loadingEvent: InheritanceWalletAuthDeviceEvent, + completedEvent: InheritanceWalletAuthDeviceEvent, + ) => { + if (walletAuthDeviceEvents[completedEvent]) return checkIconComponent; + if (walletAuthDeviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.wallet.walletAuth.actions.confirmOnDevice, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceWalletAuthDeviceEvent.INIT, + InheritanceWalletAuthDeviceEvent.CONFIRMED, + ), + }, + ]; + + if (!isRegisterationRequired) { + actions.push({ + id: '2', + text: strings.wallet.walletAuth.actions.tapCard, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceWalletAuthDeviceEvent.CONFIRMED, + InheritanceWalletAuthDeviceEvent.WALLET_BASED_CARD_TAPPED, + ), + }); + } + + return actions; + }, [strings, walletAuthDeviceEvents, isRegisterationRequired]); + + useEffect(() => { + const eventToState: Record = { + [InheritanceWalletAuthDeviceEvent.CONFIRMED]: 0, + [InheritanceWalletAuthDeviceEvent.SEED_BASED_CARD_TAPPED]: 1, + [InheritanceWalletAuthDeviceEvent.CARD_PAIRING_CARD_TAPPED]: 2, + [InheritanceWalletAuthDeviceEvent.WALLET_BASED_CARD_TAPPED]: 3, + }; + + let state: number | undefined; + for (const event in eventToState) { + if (walletAuthDeviceEvents[event] && eventToState[event] !== undefined) { + state = eventToState[event]!; + } + } + + if (state !== undefined) { + setCardTapState(state); + } + }, [walletAuthDeviceEvents]); + + useEffect(() => { + clearErrors(); + walletAuthStart(); + setCardTapState(-1); + + return () => { + walletAuthAbort(); + }; + }, [retryIndex, clearErrors]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.walletAuth) { + onNext(); + } + }, [walletAuthStep]); + + return ( + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/index.tsx new file mode 100644 index 000000000..b4ec4f637 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/WalletAuth/index.tsx @@ -0,0 +1,3 @@ +export * from './FetchRequestId'; +export * from './WalletAuth'; +export * from './ValidateSignature'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/index.tsx new file mode 100644 index 000000000..661e70362 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Dialogs/index.tsx @@ -0,0 +1,12 @@ +export * from './Terms'; +export * from './Ensure'; +export * from './Instructions'; +export * from './SelectWallet'; +export * from './WalletAuth'; +export * from './Email'; +export * from './Encryption'; +export * from './NomineeAndExecutor'; +export * from './Checkout'; +export * from './Message'; +export * from './Summary'; +export * from './Greeting'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Layout.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Layout.tsx new file mode 100644 index 000000000..eb50005af --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/Layout.tsx @@ -0,0 +1,31 @@ +import { + DialogBox, + DialogBoxBody, + DialogBoxFooter, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +export interface LayoutProps { + children: React.ReactNode; + footerComponent?: React.ReactNode; +} + +export const Layout: React.FC = ({ + children, + footerComponent, +}) => ( + + + {children} + + {footerComponent && ( + + {footerComponent} + + )} + +); + +Layout.defaultProps = { + footerComponent: undefined, +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/index.tsx new file mode 100644 index 000000000..ec216fcae --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/index.tsx @@ -0,0 +1,709 @@ +import { insertInheritancePlan } from '@cypherock/cysync-core-services'; +import { IInheritancePlan, IWallet } from '@cypherock/db-interfaces'; +import React, { + Context, + FC, + ReactNode, + createContext, + useCallback, + useContext, + useMemo, + useState, + useRef, + useEffect, +} from 'react'; + +import { routes } from '~/constants'; +import { + useAsync, + useMemoReturn, + useNavigateTo, + useStateWithRef, +} from '~/hooks'; +import { + inheritancePlanService, + inheritanceLoginService, + InheritanceUserTypeMap, +} from '~/services'; +import { ReminderPeriod } from '~/services/inheritance/login/schema'; +import { selectLanguage, useAppSelector } from '~/store'; +import { getDB } from '~/utils'; + +import { + InheritanceGoldPlanPurchaseDialogContextInterface, + IWalletForSelection, +} from './types'; +import { tabIndicies, useGoldPlanDialogHanlders } from './useDialogHandler'; +import { useExecutorRegistration } from './useExecutorRegistraion'; +import { useNomineeRegistration } from './useNomineeRegistration'; + +import { + useEncryptMessage, + useSession, + useWalletAuth, + WalletAuthLoginStep, +} from '../../hooks'; + +export interface IUserDetails { + name: string; + email: string; + alternateEmail: string; +} + +export const InheritanceGoldPlanPurchaseDialogContext: Context = + createContext( + {} as InheritanceGoldPlanPurchaseDialogContextInterface, + ); + +export interface InheritanceGoldPlanPurchaseDialogContextProviderProps { + children: ReactNode; +} + +export const InheritanceGoldPlanPurchaseDialogProvider: FC< + InheritanceGoldPlanPurchaseDialogContextProviderProps +> = ({ children }) => { + const { + onClose, + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + tabs, + } = useGoldPlanDialogHanlders(); + + const wallets = useAppSelector(state => state.wallet.wallets); + const lang = useAppSelector(selectLanguage); + const deletedWallets = useAppSelector(state => state.wallet.deletedWallets); + const plans = useAppSelector(state => state.inheritance.inheritancePlans); + + const isPlanActive = (plan: IInheritancePlan, walletId: string) => { + const now = Date.now(); + return ( + plan.walletId === walletId && + plan.purchasedAt && + plan.purchasedAt <= now && + plan.expireAt && + plan.expireAt >= now + ); + }; + + const allWallets = useMemo(() => { + const deletedWalletIds = deletedWallets.map(e => e.__id); + return [ + ...wallets.map(e => ({ + ...e, + isDeleted: deletedWalletIds.includes(e.__id), + isActive: plans.some(plan => e.__id && isPlanActive(plan, e.__id)), + })), + ]; + }, [wallets, deletedWallets, plans]); + + const [selectedWallet, setSelectedWallet] = useState(); + const [isSubmittingReminderDetails, setIsSubmittingReminderDetails] = + useState(false); + const [unhandledError, setUnhandledError] = useState(); + + const navigateTo = useNavigateTo(); + + const [isTermsAccepted, setIsTermsAccepted] = useState(false); + const [retryIndex, setRetryIndex] = useState(0); + const [coupon, setCoupon, couponRef] = useStateWithRef(''); + const [applyingCouponError, setApplyingCouponError] = useState< + { heading: string; subtext: string } | undefined + >(); + const [couponDuration, setCouponDuration] = useState(''); + const [reminderPeriod, setReminderPeriod] = + useState('monthly'); + + const onReminderDetailsSubmit = async () => { + setIsSubmittingReminderDetails(true); + try { + if (!authTokenConfig) throw "Wallet auth doesn't have a valid token"; + + const result = await inheritanceLoginService.updateReminder({ + frequency: reminderPeriod, + authTokenConfig, + }); + + if (result.result?.success === false) { + throw result.error ?? 'ReminderPeriod update failed'; + } + + if (!userDetails) { + const planDetailsResult = await inheritancePlanService.getPlan({ + authTokenConfig, + }); + + if (planDetailsResult.error) { + throw result.error ?? "Couldn't fetch plan details"; + } + const fetchedDetails = { + name: planDetailsResult.result.fullName, + ...planDetailsResult.result.owner, + } as IUserDetails; + + setUserDetails(fetchedDetails); + } + + onNext(); + } catch (error: any) { + onError(error); + } + setIsSubmittingReminderDetails(false); + }; + + const [isFetchingDetails, setIsFetchingDetails] = useState(false); + + const fetchExistingDetailsFromServer = async () => { + if (isFetchingDetails) return; + if (!authTokenConfig) throw "Wallet auth doesn't have a valid token"; + setIsFetchingDetails(true); + const result = await inheritancePlanService.getPlan({ + authTokenConfig, + }); + + if (result.error) { + setIsFetchingDetails(false); + throw result.error ?? "Couldn't fetch plan details"; + } + const fetchedDetails = { + ...result.result.owner, + } as IUserDetails; + setUserDetails(fetchedDetails); + + const fetchedNomineeDetails = result.result.nominee; + if (fetchedNomineeDetails) { + setNomineeCount(fetchedNomineeDetails.length); + fetchedNomineeDetails.forEach((details, index) => + updateNomineeDetails(details as IUserDetails, index), + ); + } + + const fetchedExecutorDetails = result.result.executor; + if (fetchedExecutorDetails) { + const nomineeIndex = Math.max( + fetchedNomineeDetails + ?.map(details => details.email) + .indexOf(fetchedExecutorDetails.nominee?.[0]) ?? 0, + 0, + ); + setHaveExecutor(true); + updateExecutorFields( + fetchedExecutorDetails as IUserDetails, + nomineeIndex, + ); + } + + if (result.result.emailConfig?.frequency) + setReminderPeriod( + (result.result.emailConfig?.frequency as ReminderPeriod) ?? 'monthly', + ); + setIsFetchingDetails(false); + }; + + const onError = useCallback((e?: any) => { + setUnhandledError(e); + }, []); + + const clearErrors = useCallback(() => { + setUnhandledError(undefined); + }, []); + + const [userDetailPrefillData, setUserDetailPrefillData] = + useState({ + name: '', + email: '', + alternateEmail: '', + }); + + const walletAuthService = useWalletAuth(onError, setUserDetailPrefillData); + const encryptMessageService = useEncryptMessage(onError); + const sessionService = useSession(onError); + const sessionIdRef = useRef(); + + const authTokenConfig = useMemo( + () => walletAuthService.authTokenConfig, + [walletAuthService], + ); + const onRetryFuncMap = useMemo< + Record boolean) | undefined> | undefined> + >( + () => ({ + [tabIndicies.instructions.tabNumber]: { + [tabIndicies.instructions.dialogs.video]: () => true, + }, + [tabIndicies.wallet.tabNumber]: { + [tabIndicies.wallet.dialogs.fetchRequestId]: () => true, + [tabIndicies.wallet.dialogs.walletAuth]: () => true, + [tabIndicies.wallet.dialogs.validateSignature]: () => true, + }, + [tabIndicies.encryption.tabNumber]: { + [tabIndicies.encryption.dialogs.deviceEncryption]: () => true, + [tabIndicies.encryption.dialogs.encryptionLoader]: () => true, + }, + [tabIndicies.message.tabNumber]: { + [tabIndicies.message.dialogs.video]: () => true, + }, + }), + [], + ); + + const onRetry = useCallback(() => { + const retryLogic = onRetryFuncMap[currentTab]?.[currentDialog]; + if (retryLogic) { + setRetryIndex(v => v + 1); + retryLogic(); + } else { + resetAll(); + goTo(0); + } + + setUnhandledError(undefined); + }, [currentTab, currentDialog, onRetryFuncMap, walletAuthService.reset]); + + const walletAuthFetchRequestId = useCallback(() => { + if (!selectedWallet?.__id) { + return; + } + + walletAuthService.fetchRequestId( + selectedWallet.__id, + InheritanceUserTypeMap.owner, + 'seed-based', + true, + ); + }, [selectedWallet, walletAuthService.fetchRequestId]); + + const encryptPinStart = useCallback(async () => { + if (!selectedWallet?.__id) { + return; + } + + let sessionId = await sessionService.getIsActive(); + + if (!sessionId) { + sessionId = await sessionService.start(); + } + + sessionIdRef.current = sessionId; + + if (sessionId) { + encryptMessageService.start(selectedWallet.__id, { + personalMessage, + cardLocation, + }); + } + }, [selectedWallet, encryptMessageService, sessionService]); + + const setupPlanHandler = useCallback(async () => { + if ( + !encryptMessageService.encryptedMessages || + !authTokenConfig || + !selectedWallet || + !sessionIdRef.current + ) + return false; + + const result = await inheritancePlanService.create({ + encryptedData: encryptMessageService.encryptedMessages, + sessionId: sessionIdRef.current, + authTokenConfig, + }); + + if (result.error) { + throw result.error; + } + + const db = getDB(); + await insertInheritancePlan(db, { + walletId: selectedWallet.__id ?? '', + isNominee: false, + type: 'gold', + walletName: selectedWallet.name, + }); + + return true; + }, [ + encryptMessageService.encryptedMessages, + authTokenConfig, + selectedWallet, + ]); + + const [setupPlan, isSettingUpPlan, isSetupPlanCompleted, resetSetupPlan] = + useAsync(setupPlanHandler, onError); + + const applyCouponHandler = useCallback( + async (_coupon: string) => { + setApplyingCouponError(undefined); + + if (!authTokenConfig) return false; + + try { + const result = await inheritancePlanService.checkCoupon({ + coupon: _coupon, + authTokenConfig, + }); + + if (result.error) throw result.error; + + setCouponDuration(result.result?.duration ?? ''); + setCoupon(_coupon); + } catch (error) { + setApplyingCouponError({ + heading: lang.strings.inheritance.dialog.payment.error.errorHeading, + subtext: lang.strings.inheritance.dialog.payment.error.subtext, + }); + return false; + } + + return true; + }, + [authTokenConfig, onNext], + ); + + const [applyCoupon, isApplyingCoupon, isCouponApplied, resetApplyCoupon] = + useAsync(applyCouponHandler, onError); + + const activateCouponHandler = useCallback(async () => { + if (!authTokenConfig || !couponRef.current || !selectedWallet) return false; + + const result = await inheritancePlanService.activate({ + coupon: couponRef.current, + authTokenConfig, + }); + + if (result.error) { + throw result.error; + } + + const db = getDB(); + await insertInheritancePlan(db, { + walletId: selectedWallet.__id ?? '', + isNominee: false, + type: 'gold', + walletName: selectedWallet.name, + purchasedAt: Date.now(), + expireAt: + Date.now() + + 1000 * 60 * 60 * 24 * 365 * parseInt(couponDuration.split(' ')[0], 10), + }); + + return true; + }, [authTokenConfig, selectedWallet, couponDuration]); + + const [ + activateCoupon, + isActivatingCoupon, + isCouponActivated, + resetActivateCoupon, + ] = useAsync(activateCouponHandler, onError); + + const removeCoupon = useCallback(() => { + setCoupon(''); + resetApplyCoupon(); + resetActivateCoupon(); + }, []); + + const resetAll = useCallback(() => { + resetSetupPlan(); + resetApplyCoupon(); + resetActivateCoupon(); + setApplyingCouponError(undefined); + setCoupon(''); + setCouponDuration(''); + setSelectedWallet(undefined); + setRetryIndex(v => v + 1); + walletAuthService.reset(); + encryptMessageService.reset(); + sessionService.reset(); + }, [walletAuthService.reset, encryptMessageService.reset, resetSetupPlan]); + + const registerUser = useCallback( + async (params: IUserDetails) => { + const isSuccess = await walletAuthService.registerUser(params); + + if (isSuccess) { + setUserDetails(params); + goTo(tabIndicies.owner.tabNumber, tabIndicies.owner.dialogs.verifyOtp); + } + }, + [walletAuthService.registerUser], + ); + + const [personalMessage, setPersonalMessage] = useState(''); + const [cardLocation, setCardLocation] = useState(''); + const [userDetails, setUserDetails] = useState(); + const [isOnSummaryPage, setIsOnSummaryPage] = useState(false); + + const { + onNomineeDetailsSubmit, + isSubmittingNomineeDetails, + nomineeCount, + setNomineeCount, + nomineeDetails, + updateNomineeDetails, + nomineeOtpSubmit, + clearNomineeDetails, + nomineeOtpVerificationDetails, + nominees, + } = useNomineeRegistration( + onError, + onNext, + goTo, + isOnSummaryPage, + authTokenConfig, + ); + + useEffect(() => { + if (nomineeCount === 1 && authTokenConfig !== undefined) { + inheritanceLoginService.clearMetaData({ + resetNominee: true, + authTokenConfig, + }); + } + }, [nomineeCount]); + + const { + haveExecutor, + setHaveExecutor, + onExecutorSelected, + onExecutorDetailsSubmit, + isSubmittingExecutorDetails, + executorNomineeIndex, + executorDetails, + executorMessage, + setExecutorMessage, + onExecutorMessageSubmit, + updateExecutorFields, + } = useExecutorRegistration( + onError, + onNext, + goTo, + isOnSummaryPage, + nominees, + authTokenConfig, + ); + + useEffect(() => { + if (haveExecutor === false && authTokenConfig !== undefined) { + inheritanceLoginService.clearMetaData({ + resetExecutor: true, + authTokenConfig, + }); + } + }, [haveExecutor]); + + const onNextActionMapPerDialog = useMemo< + Record boolean) | undefined> | undefined> + >( + () => ({ + [tabIndicies.wallet.tabNumber]: { + [tabIndicies.wallet.dialogs.fetchRequestId]: () => { + if (walletAuthService.currentStep === WalletAuthLoginStep.completed) { + goTo(tabIndicies.nominieeAndExecutor.tabNumber); + return true; + } + + return false; + }, + [tabIndicies.wallet.dialogs.validateSignature]: () => { + if (!walletAuthService.isRegisterationRequired) { + goTo( + tabIndicies.owner.tabNumber, + tabIndicies.owner.dialogs.verifyOtp, + ); + return true; + } + + return false; + }, + }, + }), + [walletAuthService.isRegisterationRequired, walletAuthService.currentStep], + ); + + const fallbackToWalletSelect = useCallback(() => { + resetSetupPlan(); + resetApplyCoupon(); + resetActivateCoupon(); + setRetryIndex(v => v + 1); + walletAuthService.reset(); + encryptMessageService.reset(); + goTo(tabIndicies.wallet.tabNumber, tabIndicies.wallet.dialogs.selectWallet); + }, [walletAuthService.reset, encryptMessageService.reset, resetSetupPlan]); + + const onNextCallback = useCallback(() => { + const action = onNextActionMapPerDialog[currentTab]?.[currentDialog]; + + let doNext = true; + + if (action) { + doNext = !action(); + } + + if (doNext) { + onNext(); + } + }, [onNext, currentTab, currentDialog]); + + const onPreviousActionMapPerDialog = useMemo< + Record boolean) | undefined> | undefined> + >( + () => ({ + [tabIndicies.owner.tabNumber]: { + [tabIndicies.owner.dialogs.userDetails]: () => { + fallbackToWalletSelect(); + return true; + }, + [tabIndicies.owner.dialogs.verifyOtp]: () => { + fallbackToWalletSelect(); + return true; + }, + }, + [tabIndicies.message.tabNumber]: { + [tabIndicies.message.dialogs.video]: () => { + if (!haveExecutor) { + goTo( + tabIndicies.nominieeAndExecutor.tabNumber, + tabIndicies.nominieeAndExecutor.dialogs.executorSelect, + ); + return true; + } + return false; + }, + }, + [tabIndicies.nominieeAndExecutor.tabNumber]: { + [tabIndicies.nominieeAndExecutor.dialogs.nomineeCountSelect]: () => { + fallbackToWalletSelect(); + return true; + }, + }, + }), + [fallbackToWalletSelect, haveExecutor], + ); + + const onPreviousCallback = useCallback(() => { + const action = onPreviousActionMapPerDialog[currentTab]?.[currentDialog]; + + let doPrevious = true; + + if (action) { + doPrevious = !action(); + } + + if (doPrevious) { + onPrevious(); + } + }, [onPrevious, currentTab, currentDialog]); + + const onCloseCallback = useCallback(() => { + if (isSetupPlanCompleted || isCouponActivated) { + navigateTo(routes.inheritance.home.path); + } + + onClose(); + }, [isSetupPlanCompleted, isCouponActivated, onClose]); + + const overriddenCurrentMilestone = useMemo( + () => (isOnSummaryPage ? tabIndicies.summary.tabNumber : undefined), + [isOnSummaryPage], + ); + + const ctx = useMemoReturn({ + onNext: onNextCallback, + onPrevious: onPreviousCallback, + onClose: onCloseCallback, + tabs, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + allWallets, + selectedWallet, + setSelectedWallet, + registerUser, + unhandledError, + onRetry, + retryIndex, + clearErrors, + walletAuthDeviceEvents: walletAuthService.deviceEvents, + walletAuthFetchRequestId, + walletAuthIsFetchingRequestId: walletAuthService.isFetchingRequestId, + walletAuthStart: walletAuthService.startWalletAuth, + walletAuthValidateSignature: walletAuthService.validateSignature, + walletAuthIsValidatingSignature: walletAuthService.isValidatingSignature, + walletAuthStep: walletAuthService.currentStep, + walletAuthAbort: walletAuthService.abortWalletAuth, + onRegister: walletAuthService.registerUser, + isRegisteringUser: walletAuthService.isRegisteringUser, + otpVerificationDetails: walletAuthService.otpVerificationDetails, + verifyOtp: walletAuthService.verifyOtp, + isVerifyingOtp: walletAuthService.isVerifyingOtp, + encryptPinStart, + encryptPinAbort: encryptMessageService.abort, + encryptPinDeviceEvents: encryptMessageService.deviceEvents, + encryptPinIsCompleted: encryptMessageService.isEncrypted, + setupPlan, + isSettingUpPlan, + isSetupPlanCompleted, + isTermsAccepted, + setIsTermsAccepted, + coupon, + applyCoupon, + isApplyingCoupon, + isCouponApplied, + activateCoupon, + isActivatingCoupon, + isCouponActivated, + removeCoupon, + applyingCouponError, + couponDuration, + onNomineeDetailsSubmit, + isSubmittingNomineeDetails, + nomineeCount, + setNomineeCount, + personalMessage, + setPersonalMessage, + cardLocation, + setCardLocation, + nomineeDetails, + userDetails, + isEstablishingSession: sessionService.isStartingSession, + isRegisterationRequired: walletAuthService.isRegisterationRequired, + reminderPeriod, + setReminderPeriod, + isSubmittingReminderDetails, + onReminderDetailsSubmit, + updateNomineeDetails, + nomineeOtpSubmit, + clearNomineeDetails, + nomineeOtpVerificationDetails, + overriddenCurrentMilestone, + isOnSummaryPage, + setIsOnSummaryPage, + haveExecutor, + setHaveExecutor, + onExecutorSelected, + onExecutorDetailsSubmit, + isSubmittingExecutorDetails, + executorNomineeIndex, + executorDetails, + executorMessage, + setExecutorMessage, + onExecutorMessageSubmit, + fetchExistingDetailsFromServer, + userDetailPrefillData, + }); + + return ( + + {children} + + ); +}; + +export function useInheritanceGoldPlanPurchaseDialog(): InheritanceGoldPlanPurchaseDialogContextInterface { + return useContext(InheritanceGoldPlanPurchaseDialogContext); +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/types.ts b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/types.ts new file mode 100644 index 000000000..12b727ada --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/types.ts @@ -0,0 +1,100 @@ +import { IWallet } from '@cypherock/db-interfaces'; + +import { ITabs } from '~/hooks'; +import { ReminderPeriod } from '~/services/inheritance/login/schema'; + +import { + IUserDetails, + WalletAuthLoginStep, + IOtpVerificationDetails, +} from '../../hooks'; + +export interface IWalletForSelection extends IWallet { + isDeleted?: boolean; + isActive?: boolean; +} + +export interface InheritanceGoldPlanPurchaseDialogContextInterface { + tabs: ITabs; + onNext: (tab?: number, dialog?: number) => void; + goTo: (tab: number, dialog?: number) => void; + onPrevious: () => void; + currentTab: number; + currentDialog: number; + isDeviceRequired: boolean; + onClose: () => void; + allWallets: IWalletForSelection[]; + selectedWallet?: IWalletForSelection; + setSelectedWallet: (wallet: IWalletForSelection) => void; + registerUser: (params: IUserDetails) => void; + isRegisteringUser: boolean; + unhandledError?: any; + onRetry: () => void; + retryIndex: number; + clearErrors: () => void; + walletAuthDeviceEvents: Record; + walletAuthFetchRequestId: () => void; + walletAuthIsFetchingRequestId: boolean; + walletAuthStart: () => void; + walletAuthAbort: () => void; + walletAuthIsValidatingSignature: boolean; + walletAuthValidateSignature: () => Promise; + walletAuthStep: WalletAuthLoginStep; + otpVerificationDetails?: IOtpVerificationDetails; + verifyOtp: (otp: string) => Promise; + isVerifyingOtp: boolean; + encryptPinStart: () => void; + encryptPinAbort: () => void; + encryptPinDeviceEvents: Record; + encryptPinIsCompleted: boolean; + setupPlan: () => void; + isSettingUpPlan: boolean; + isSetupPlanCompleted: boolean; + isTermsAccepted: boolean; + setIsTermsAccepted: (value: boolean) => void; + coupon: string; + applyCoupon: (coupon: string) => Promise; + isApplyingCoupon: boolean; + isCouponApplied: boolean; + activateCoupon: () => Promise; + isActivatingCoupon: boolean; + isCouponActivated: boolean; + removeCoupon: () => void; + applyingCouponError?: { heading: string; subtext: string }; + couponDuration: string; + onNomineeDetailsSubmit: (params: boolean, index: number) => void; + isSubmittingNomineeDetails: boolean; + nomineeCount: number; + setNomineeCount: (nomineeCount: number) => void; + haveExecutor: boolean; + setHaveExecutor: (val: boolean) => void; + onExecutorSelected: () => void; + isSubmittingExecutorDetails: boolean; + onExecutorDetailsSubmit: (params: IUserDetails, index: number) => void; + personalMessage: string; + setPersonalMessage: (val: string) => void; + cardLocation: string; + setCardLocation: (val: string) => void; + executorMessage: string; + setExecutorMessage: (val: string) => void; + nomineeDetails: Record; + userDetails?: IUserDetails; + isEstablishingSession: boolean; + isRegisterationRequired: boolean; + reminderPeriod: ReminderPeriod; + setReminderPeriod: (val: ReminderPeriod) => void; + isSubmittingReminderDetails: boolean; + onReminderDetailsSubmit: () => Promise; + nomineeOtpSubmit: (val: string) => Promise; + clearNomineeDetails: () => void; + updateNomineeDetails: (params: IUserDetails, index: number) => void; + nomineeOtpVerificationDetails?: IOtpVerificationDetails; + onExecutorMessageSubmit: () => Promise; + executorDetails?: IUserDetails; + overriddenCurrentMilestone?: number; + isOnSummaryPage: boolean; + setIsOnSummaryPage: (val: boolean) => void; + executorNomineeIndex?: number; + fetchExistingDetailsFromServer: () => void; + userDetailPrefillData: IUserDetails; +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/useDialogHandler.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/useDialogHandler.tsx new file mode 100644 index 000000000..e14784b85 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/useDialogHandler.tsx @@ -0,0 +1,244 @@ +import React, { useMemo } from 'react'; + +import { ITabs, useTabsAndDialogs } from '~/hooks'; +import { + useAppDispatch, + useAppSelector, + selectLanguage, + closeDialog, +} from '~/store'; + +import { + DeviceEncryption, + EncryptionLoader, + EncryptionSuccess, + Ensure, + Instructions, + SelectWallet, + Terms, + UserDetails, + VerifyOTP, + WalletAuth, + Nominee, + NomineeDetails, + VerifyNomineeOtp, + ConfirmNomineeVerification, + ExecutorDetails, + SelectExecutor, + ExecutorMessageTutorial, + ExecutorMessage, + ExecutorReminderSetup, + Checkout, + NomineePrivateMessageInput, + Greeting, + Summary, + FetchRequestId, + ValidateSignature, +} from '../Dialogs'; + +export const tabIndicies = { + terms: { + tabNumber: 0, + dialogs: { + terms: 0, + }, + }, + instructions: { + tabNumber: 1, + dialogs: { + ensure: 0, + video: 1, + }, + }, + wallet: { + tabNumber: 2, + dialogs: { + selectWallet: 0, + fetchRequestId: 1, + walletAuth: 2, + validateSignature: 3, + }, + }, + owner: { + tabNumber: 3, + dialogs: { + userDetails: 0, + verifyOtp: 1, + }, + }, + nominieeAndExecutor: { + tabNumber: 4, + dialogs: { + nomineeCountSelect: 0, + firstNomineeDetails: 1, + firstNomineeConfirmOtp: 2, + firstNomineeVerifyOtp: 3, + secondNomineeDetails: 4, + secondNomineeConfirmOtp: 5, + secondNomineeVerifyOtp: 6, + executorSelect: 7, + executorDetails: 8, + }, + }, + message: { + tabNumber: 5, + dialogs: { + video: 0, + personalMessageInput: 1, + executorMessageInput: 2, + }, + }, + reminder: { + tabNumber: 6, + dialogs: { + reminderInput: 0, + }, + }, + summary: { + tabNumber: 7, + dialogs: { + summary: 0, + }, + }, + encryption: { + tabNumber: 8, + dialogs: { + deviceEncryption: 0, + encryptionLoader: 1, + encryptionSuccess: 2, + }, + }, + checkout: { + tabNumber: 9, + dialogs: { + checkout: 0, + }, + }, +}; +export const useGoldPlanDialogHanlders = () => { + const dispatch = useAppDispatch(); + const lang = useAppSelector(selectLanguage); + + const deviceRequiredDialogsMap: Record = + useMemo( + () => ({ + [tabIndicies.wallet.tabNumber]: [tabIndicies.wallet.dialogs.walletAuth], + [tabIndicies.encryption.tabNumber]: [ + tabIndicies.encryption.dialogs.deviceEncryption, + ], + }), + [], + ); + + const tabs: ITabs = useMemo( + () => [ + { + name: lang.strings.inheritance.termsOfService.title, + dialogs: [], + }, + { + name: lang.strings.inheritanceGoldPlanPurchase.instructions.heading, + dialogs: [, ], + }, + { + name: lang.strings.inheritanceGoldPlanPurchase.wallet.heading, + dialogs: [ + , + , + , + , + ], + }, + { + name: lang.strings.inheritanceGoldPlanPurchase.email.heading, + dialogs: [ + , + , + ], + }, + { + name: lang.strings.inheritanceGoldPlanPurchase.nomineeAndExecutor + .heading, + dialogs: [ + , + , + , + , + , + , + , + , + , + ], + }, + { + name: lang.strings.inheritanceGoldPlanPurchase.message.heading, + dialogs: [ + , + , + , + ], + }, + { + name: lang.strings.inheritanceGoldPlanPurchase.reminder.heading, + dialogs: [], + }, + { + name: lang.strings.inheritanceGoldPlanPurchase.summary.heading, + dialogs: [], + }, + { + name: lang.strings.inheritanceGoldPlanPurchase.encryption.heading, + dialogs: [ + , + , + , + ], + }, + { + name: lang.strings.inheritanceGoldPlanPurchase.checkout.heading, + dialogs: [], + }, + { + name: '', + dialogs: [], + dontShowOnMilestone: true, + }, + ], + [], + ); + + const { + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + } = useTabsAndDialogs({ + deviceRequiredDialogsMap, + tabs, + dialogName: 'inheritanceGoldPlanPurchase', + }); + + const onClose = () => { + dispatch(closeDialog('inheritanceGoldPlanPurchase')); + }; + + return { + onClose, + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + tabs, + }; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/useExecutorRegistraion.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/useExecutorRegistraion.tsx new file mode 100644 index 000000000..005596f18 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/useExecutorRegistraion.tsx @@ -0,0 +1,92 @@ +import { useState } from 'react'; + +import { useMemoReturn, useStateWithRef } from '~/hooks'; +import { inheritanceLoginService } from '~/services'; +import { AuthTokenConfig } from '~/services/utils'; + +import { tabIndicies } from './useDialogHandler'; + +import { IUserDetails } from '.'; + +export const useExecutorRegistration = ( + onError: (e?: any) => void, + onNext: () => [number, number], + goTo: (tab: number, dialog?: number) => void, + isOnSummaryPage: boolean, + nominees: React.MutableRefObject>, + authTokenConfig?: AuthTokenConfig, +) => { + const [executorDetails, setExecutorDetails, executorDetailsRef] = + useStateWithRef(undefined); + const [haveExecutor, setHaveExecutor] = useState(false); + const [isSubmittingExecutorDetails, setIsSubmittingExecutorDetails] = + useState(false); + const [executorNomineeIndex, setExecutorNomineeIndex] = useState< + number | undefined + >(); + const [executorMessage, setExecutorMessage] = useState(''); + + const onExecutorSelected = () => { + if (haveExecutor) onNext(); + else goTo(tabIndicies.message.tabNumber, tabIndicies.message.dialogs.video); + }; + + const updateExecutor = async (nomineeIndex: number) => { + try { + if (!executorDetailsRef.current) throw 'Invalid executor details'; + if (!authTokenConfig) throw "Wallet auth doesn't have a valid token"; + + const result = await inheritanceLoginService.updateExecutor({ + name: executorDetailsRef.current.name, + email: executorDetailsRef.current.email, + alternateEmail: executorDetailsRef.current.alternateEmail, + nomineeEmail: nominees.current[nomineeIndex].email, + executorMessage, + authTokenConfig, + }); + + if (!result.result?.success) { + throw result.error; + } + if (isOnSummaryPage) goTo(tabIndicies.summary.tabNumber); + else onNext(); + } catch (error: any) { + onError(error); + } + }; + + const onExecutorMessageSubmit = async () => { + setIsSubmittingExecutorDetails(true); + await updateExecutor(executorNomineeIndex ?? 0); + setIsSubmittingExecutorDetails(false); + }; + + const onExecutorDetailsSubmit = async ( + params: IUserDetails, + nomineeIndex: number, + ) => { + setIsSubmittingExecutorDetails(true); + updateExecutorFields(params, nomineeIndex); + await updateExecutor(nomineeIndex); + setIsSubmittingExecutorDetails(false); + }; + + const updateExecutorFields = (params: IUserDetails, nomineeIndex: number) => { + setExecutorNomineeIndex(nomineeIndex); + setExecutorDetails(params); + }; + + return useMemoReturn({ + haveExecutor, + setHaveExecutor, + onExecutorSelected, + onExecutorDetailsSubmit, + isSubmittingExecutorDetails, + executorNomineeIndex, + executorDetails, + executorMessage, + setExecutorMessage, + onExecutorMessageSubmit, + updateExecutorFields, + }); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/useNomineeRegistration.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/useNomineeRegistration.tsx new file mode 100644 index 000000000..c7704b8fe --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/context/useNomineeRegistration.tsx @@ -0,0 +1,137 @@ +import { useRef, useState } from 'react'; + +import { useMemoReturn, useStateWithRef } from '~/hooks'; +import { inheritanceLoginService } from '~/services'; +import { AuthTokenConfig } from '~/services/utils'; + +import { tabIndicies } from './useDialogHandler'; + +import { IOtpVerificationDetails, OtpVerificationConcern } from '../../hooks'; + +import { IUserDetails } from '.'; + +export const useNomineeRegistration = ( + onError: (e?: any) => void, + onNext: () => [number, number], + goTo: (tab: number, dialog?: number) => void, + isOnSummaryPage: boolean, + authTokenConfig?: AuthTokenConfig, +) => { + const [isSubmittingNomineeDetails, setIsSubmittingNomineeDetails] = + useState(false); + const [nomineeCount, setNomineeCount, nomineeCountRef] = useStateWithRef(1); + const [nomineeDetails, setNomineeDetails, nominees] = useStateWithRef< + Record + >({}); + const [nomineeOtpVerificationDetails, setNomineeOtpVerificationDetails] = + useState(); + const nomineeOtpDetailsRef = useRef([]); + + const updateNominees = async (index: number, verify?: boolean) => { + try { + if (!authTokenConfig) throw "Wallet auth doesn't have a valid token"; + + const result = await inheritanceLoginService.updateNominees({ + nominee: nominees.current[index], + verify, + nomineeType: index === 0 ? 'PRIMARY' : 'ALTERNATE', + authTokenConfig, + }); + + if (result.result?.success === false) { + throw result.error ?? 'Nominee update failed'; + } + + if (verify) { + const otpDetails = result.result?.otpDetails; + if (!otpDetails) throw 'Invalid data received from server'; + nomineeOtpDetailsRef.current = otpDetails.map((details, idx) => { + const { requestId, maskedEmail, ...rest } = details; + return { + id: requestId, + emails: [maskedEmail], + concern: + idx === 0 + ? OtpVerificationConcern.primary + : OtpVerificationConcern.alternate, + ...rest, + }; + }); + + setNomineeOtpVerificationDetails(nomineeOtpDetailsRef.current.shift()); + } + } catch (error: any) { + onError(error); + } + }; + + const nomineeOtpSubmit = async (secret: string) => { + setIsSubmittingNomineeDetails(true); + try { + if (!authTokenConfig || !nomineeOtpVerificationDetails) + throw 'Invalid auth or data'; + + const result = await inheritanceLoginService.updateNominees({ + secret, + requestId: nomineeOtpVerificationDetails.id, + authTokenConfig, + }); + + if (result.result?.success === true) { + setNomineeOtpVerificationDetails(nomineeOtpDetailsRef.current.shift()); + } else { + const otpDetails = result.error?.details?.responseBody; + if (!otpDetails) throw 'Invalid data received from server'; + setNomineeOtpVerificationDetails(old => ({ + ...old!, + retriesRemaining: otpDetails.retriesRemaining, + otpExpiry: otpDetails.otpExpiry, + showIncorrectError: true, + })); + } + } catch (error: any) { + onError(error); + } + setIsSubmittingNomineeDetails(false); + }; + + const updateNomineeDetails = (params: IUserDetails, index: number) => { + nominees.current[index] = params; + setNomineeDetails(nominees.current); + }; + + const clearNomineeDetails = () => { + setNomineeDetails({}); + setNomineeOtpVerificationDetails(undefined); + setIsSubmittingNomineeDetails(false); + }; + + const onNomineeDetailsSubmit = async (verify: boolean, index: number) => { + setIsSubmittingNomineeDetails(true); + + await updateNominees(index, verify); + + let nextDialog = tabIndicies.nominieeAndExecutor.dialogs.executorSelect; + if (nomineeCountRef.current === 2 && index === 0) + nextDialog = tabIndicies.nominieeAndExecutor.dialogs.secondNomineeDetails; + + if (verify) onNext(); + else if (isOnSummaryPage) goTo(tabIndicies.summary.tabNumber); + else goTo(tabIndicies.nominieeAndExecutor.tabNumber, nextDialog); + + setIsSubmittingNomineeDetails(false); + }; + + return useMemoReturn({ + onNomineeDetailsSubmit, + isSubmittingNomineeDetails, + nomineeCount, + setNomineeCount, + nomineeDetails, + updateNomineeDetails, + nomineeOtpSubmit, + clearNomineeDetails, + nomineeOtpVerificationDetails, + nominees, + }); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/index.tsx new file mode 100644 index 000000000..a133c5e46 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/GoldPlanPurchase/index.tsx @@ -0,0 +1,109 @@ +import { + BlurOverlay, + CloseButton, + DialogBox, + DialogBoxBackgroundBar, + DialogBoxBody, + MilestoneAside, + WalletDialogMainContainer, +} from '@cypherock/cysync-ui'; +import React, { FC } from 'react'; + +import { + ErrorHandlerDialog, + WithConnectedDevice, + WithConnectedDeviceProps, +} from '~/components'; +import { useAppSelector, selectLanguage } from '~/store'; + +import { + InheritanceGoldPlanPurchaseDialogProvider, + useInheritanceGoldPlanPurchaseDialog, +} from './context'; + +const DeviceConnectionWrapper: React.FC<{ + isDeviceRequired: boolean; + children: React.ReactNode; + withConnectedProps?: WithConnectedDeviceProps; +}> = ({ isDeviceRequired, withConnectedProps, children }) => { + if (isDeviceRequired) + return ( + + {children} + + ); + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; +}; + +DeviceConnectionWrapper.defaultProps = { + withConnectedProps: {}, +}; + +const InheritanceGoldPlanPurchase: FC = () => { + const { + tabs, + currentTab, + currentDialog, + unhandledError, + onClose, + onRetry, + isDeviceRequired, + overriddenCurrentMilestone, + } = useInheritanceGoldPlanPurchaseDialog(); + + const lang = useAppSelector(selectLanguage); + + return ( + + + <> + !t.dontShowOnMilestone) + .map(t => t.name)} + activeTab={overriddenCurrentMilestone ?? currentTab} + heading={lang.strings.inheritanceGoldPlanPurchase.title} + /> + + + + + {tabs[currentTab]?.dialogs[currentDialog]} + + + + onClose()} />} + position="top" + useLightPadding + /> + + + + + ); +}; + +export const InheritanceGoldPlanPurchaseDialog = () => ( + + + +); diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/DecryptPin.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/DecryptPin.tsx new file mode 100644 index 000000000..7cb589ee6 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/DecryptPin.tsx @@ -0,0 +1,120 @@ +import { InheritanceDecryptMessageDeviceEvent } from '@cypherock/app-support-inheritance'; +import { + ArrowRightIcon, + Check, + Container, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + tapAnyCardDeviceAnimation2DVideo, + Throbber, + Typography, + Video, +} from '@cypherock/cysync-ui'; +import React, { useEffect } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritancePinRecoveryDialog } from '../context'; +import { Layout } from '../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const DecryptPin = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.dialogs.inheritancePinRecovery; + + const { + onNext, + retryIndex, + clearErrors, + decryptPinStart, + decryptPinDeviceEvents, + } = useInheritancePinRecoveryDialog(); + + const getDeviceEventIcon = ( + loadingEvent: InheritanceDecryptMessageDeviceEvent, + completedEvent: InheritanceDecryptMessageDeviceEvent, + ) => { + if (decryptPinDeviceEvents[completedEvent]) return checkIconComponent; + if (decryptPinDeviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.decryptPin.actions.confirmOnDevice, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceDecryptMessageDeviceEvent.INIT, + InheritanceDecryptMessageDeviceEvent.CONFIRMED, + ), + }, + { + id: '2', + text: strings.decryptPin.actions.tapCard, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceDecryptMessageDeviceEvent.CONFIRMED, + InheritanceDecryptMessageDeviceEvent.CARD_TAPPED, + ), + }, + ]; + + return actions; + }, [decryptPinDeviceEvents]); + + useEffect(() => { + clearErrors(); + decryptPinStart(); + }, [retryIndex, clearErrors]); + + useEffect(() => { + if ( + decryptPinDeviceEvents[InheritanceDecryptMessageDeviceEvent.CARD_TAPPED] + ) { + setTimeout(() => onNext(), 2000); + } + }, [decryptPinDeviceEvents]); + + return ( + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/Success.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/Success.tsx new file mode 100644 index 000000000..4f0cf5ddb --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/Success.tsx @@ -0,0 +1,25 @@ +import { ConfettiBlast, SuccessDialog } from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritancePinRecoveryDialog } from '../context'; + +export const SuccessPinRecovery = () => { + const lang = useAppSelector(selectLanguage); + const { onClose } = useInheritancePinRecoveryDialog(); + + const strings = lang.strings.dialogs.inheritancePinRecovery.success; + + return ( + <> + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/ViewPin.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/ViewPin.tsx new file mode 100644 index 000000000..354a8b4c1 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/ViewPin.tsx @@ -0,0 +1,100 @@ +import { InheritanceDecryptMessageDeviceEvent } from '@cypherock/app-support-inheritance'; +import { + ArrowRightIcon, + Check, + Container, + Image, + LangDisplay, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + Throbber, + Typography, + verifyPinOnDevice, +} from '@cypherock/cysync-ui'; +import React, { useEffect } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritancePinRecoveryDialog } from '../context'; +import { Layout } from '../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const ViewPin = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.dialogs.inheritancePinRecovery.viewPin; + + const { + onNext, + decryptPinIsCompleted, + decryptPinDeviceEvents, + selectedWallet, + } = useInheritancePinRecoveryDialog(); + + const getDeviceEventIcon = ( + loadingEvent: InheritanceDecryptMessageDeviceEvent, + completedEvent: InheritanceDecryptMessageDeviceEvent, + ) => { + if (decryptPinDeviceEvents[completedEvent]) return checkIconComponent; + if (decryptPinDeviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.actions.viewDevice, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceDecryptMessageDeviceEvent.CARD_TAPPED, + InheritanceDecryptMessageDeviceEvent.PIN_VERIFIED, + ), + }, + ]; + + return actions; + }, [decryptPinDeviceEvents]); + + useEffect(() => { + if (decryptPinIsCompleted) { + onNext(); + } + }, [decryptPinIsCompleted]); + + return ( + + verify pin on device + + + + {strings.title} + + + + + {selectedWallet} + + + + + {actionsList.map(data => ( + + ))} + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/FetchData.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/FetchData.tsx new file mode 100644 index 000000000..d4a0cb17d --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/FetchData.tsx @@ -0,0 +1,25 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritancePinRecoveryDialog } from '../../context'; + +export const FetchData = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.dialogs.inheritancePinRecovery.sync.fetch; + const { onNext, retryIndex, fetchEncryptedData, isEncryptedDataFetched } = + useInheritancePinRecoveryDialog(); + + useEffect(() => { + fetchEncryptedData(); + }, [retryIndex]); + + useEffect(() => { + if (isEncryptedDataFetched) { + onNext(); + } + }, [isEncryptedDataFetched]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/FetchRequestId.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/FetchRequestId.tsx new file mode 100644 index 000000000..bd754f276 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/FetchRequestId.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; + +import { useInheritancePinRecoveryDialog } from '../../context'; + +export const FetchRequestId = () => { + const { onNext, walletAuthStep, walletAuthFetchRequestId, retryIndex } = + useInheritancePinRecoveryDialog(); + + useEffect(() => { + walletAuthFetchRequestId(); + }, [retryIndex]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.fetchRequestId) { + onNext(); + } + }, [walletAuthStep]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/ValidateSignature.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/ValidateSignature.tsx new file mode 100644 index 000000000..edfaf4f93 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/ValidateSignature.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; + +import { useInheritancePinRecoveryDialog } from '../../context'; + +export const ValidateSignature = () => { + const { onNext, walletAuthStep, walletAuthValidateSignature, retryIndex } = + useInheritancePinRecoveryDialog(); + + useEffect(() => { + walletAuthValidateSignature(); + }, [retryIndex]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.validateSignature) { + onNext(); + } + }, [walletAuthStep]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/VerifyOTP.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/VerifyOTP.tsx new file mode 100644 index 000000000..7adaf6e9d --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/VerifyOTP.tsx @@ -0,0 +1,77 @@ +import React, { useMemo, useRef, useCallback, useEffect } from 'react'; + +import { + OTPInputDialog, + OTPInputDialogRef, +} from '~/dialogs/Inheritance/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritancePinRecoveryDialog } from '../../context'; + +export const VerifyOTP: React.FC = () => { + const lang = useAppSelector(selectLanguage); + const { title } = lang.strings.dialogs.inheritancePinRecovery.sync.verifyOtp; + const { + onPrevious, + onNext, + otpVerificationDetails, + verifyOtp, + isVerifyingOtp, + onClose, + } = useInheritancePinRecoveryDialog(); + + const otpRef = useRef(null); + + const onVerify = useCallback( + async (otp: string) => { + await verifyOtp(otp); + }, + [verifyOtp], + ); + + const onResend = () => { + // DUMMY FUNCTION + }; + + const otpExpireTime = useMemo( + () => otpVerificationDetails?.otpExpiry ?? '', + [otpVerificationDetails?.otpExpiry], + ); + + const otpLength = 6; + + const retriesRemaining = otpVerificationDetails?.retriesRemaining ?? 3; + const email = otpVerificationDetails?.emails ?? ''; + + useEffect(() => { + if (otpRef.current) { + otpRef.current.reset(); + } + }, [otpVerificationDetails?.id]); + + useEffect(() => { + if (!otpVerificationDetails) onNext(); + }, [otpVerificationDetails]); + + if (!otpVerificationDetails) { + return null; + } + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/WalletAuth.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/WalletAuth.tsx new file mode 100644 index 000000000..fdd8216c5 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/WalletAuth.tsx @@ -0,0 +1,157 @@ +import { InheritanceWalletAuthDeviceEvent } from '@cypherock/app-support-inheritance'; +import { + ArrowRightIcon, + CardTapList, + Check, + Container, + LangDisplay, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + tapAnyCardDeviceAnimation2DVideo, + Throbber, + Typography, + Video, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useMemo, useState } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { WalletAuthLoginStep } from '../../../hooks'; +import { useInheritancePinRecoveryDialog } from '../../context'; +import { Layout } from '../../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; +export const WalletAuth = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.dialogs.inheritancePinRecovery.sync; + + const { + onNext, + selectedWallet, + walletAuthDeviceEvents, + walletAuthStep, + walletAuthStart, + walletAuthAbort, + retryIndex, + clearErrors, + } = useInheritancePinRecoveryDialog(); + + const [cardTapState, setCardTapState] = useState(-1); + + const getDeviceEventIcon = ( + loadingEvent: InheritanceWalletAuthDeviceEvent, + completedEvent: InheritanceWalletAuthDeviceEvent, + ) => { + if (walletAuthDeviceEvents[completedEvent]) return checkIconComponent; + if (walletAuthDeviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.walletAuth.actions.confirmAuth, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceWalletAuthDeviceEvent.INIT, + InheritanceWalletAuthDeviceEvent.CONFIRMED, + ), + }, + ]; + + return actions; + }, [strings, walletAuthDeviceEvents]); + + useEffect(() => { + const eventToState: Record = { + [InheritanceWalletAuthDeviceEvent.CONFIRMED]: 0, + [InheritanceWalletAuthDeviceEvent.CARD_PAIRING_CARD_TAPPED]: 1, + [InheritanceWalletAuthDeviceEvent.WALLET_BASED_CARD_TAPPED]: 2, + }; + + let state: number | undefined; + for (const event in eventToState) { + if (walletAuthDeviceEvents[event] && eventToState[event] !== undefined) { + state = eventToState[event]!; + } + } + + if (state !== undefined) { + setCardTapState(state); + } + }, [walletAuthDeviceEvents]); + + useEffect(() => { + clearErrors(); + walletAuthStart(); + + return () => { + walletAuthAbort(); + }; + }, [retryIndex, clearErrors]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.walletAuth) { + onNext(); + } + }, [walletAuthStep]); + + return ( + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/index.tsx new file mode 100644 index 000000000..215337c7a --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/WalletAuth/index.tsx @@ -0,0 +1,5 @@ +export * from './FetchRequestId'; +export * from './WalletAuth'; +export * from './ValidateSignature'; +export * from './VerifyOTP'; +export * from './FetchData'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/index.tsx new file mode 100644 index 000000000..95bb4eeda --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Dialogs/index.tsx @@ -0,0 +1,4 @@ +export * from './DecryptPin'; +export * from './Success'; +export * from './ViewPin'; +export * from './WalletAuth'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Layout.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Layout.tsx new file mode 100644 index 000000000..eb50005af --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/Layout.tsx @@ -0,0 +1,31 @@ +import { + DialogBox, + DialogBoxBody, + DialogBoxFooter, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +export interface LayoutProps { + children: React.ReactNode; + footerComponent?: React.ReactNode; +} + +export const Layout: React.FC = ({ + children, + footerComponent, +}) => ( + + + {children} + + {footerComponent && ( + + {footerComponent} + + )} + +); + +Layout.defaultProps = { + footerComponent: undefined, +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/context/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/context/index.tsx new file mode 100644 index 000000000..cbfa222ae --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/context/index.tsx @@ -0,0 +1,273 @@ +import { assert } from '@cypherock/cysync-utils'; +import React, { + Context, + FC, + ReactNode, + createContext, + useCallback, + useContext, + useMemo, + useRef, + useState, +} from 'react'; + +import { openContactSupportDialog } from '~/actions'; +import { ITabs, useAsync, useMemoReturn, useTabsAndDialogs } from '~/hooks'; +import { InheritanceUserTypeMap, inheritancePlanService } from '~/services'; +import { + closeDialog, + selectLanguage, + useAppDispatch, + useAppSelector, +} from '~/store'; + +import { + InheritancePinRecoveryDialogContextInterface, + tabIndicies, +} from './types'; + +import { useDecryptMessage, useSession, useWalletAuth } from '../../hooks'; +import { + ViewPin, + FetchData, + SuccessPinRecovery, + DecryptPin, + WalletAuth, + FetchRequestId, + ValidateSignature, + VerifyOTP, +} from '../Dialogs'; + +export * from './types'; + +export const InheritancePinRecoveryDialogContext: Context = + createContext( + {} as InheritancePinRecoveryDialogContextInterface, + ); + +export interface InheritancePinRecoveryDialogProps { + walletId: string; + walletName: string; +} + +export interface InheritancePinRecoveryDialogContextProviderProps + extends InheritancePinRecoveryDialogProps { + children: ReactNode; +} + +export const InheritancePinRecoveryDialogProvider: FC< + InheritancePinRecoveryDialogContextProviderProps +> = ({ children, walletId, walletName }) => { + const lang = useAppSelector(selectLanguage); + const dispatch = useAppDispatch(); + const selectedWallet = walletName; + const userDetails = undefined; + + const deviceRequiredDialogsMap: Record = + useMemo( + () => ({ + [tabIndicies.sync.tabNumber]: [ + tabIndicies.sync.dialogs.walletAuth, + tabIndicies.sync.dialogs.fetchData, + ], + }), + [], + ); + + const tabs: ITabs = [ + { + name: lang.strings.dialogs.inheritancePinRecovery.sync.name, + dialogs: [ + , + , + , + , + , + ], + }, + { + name: lang.strings.dialogs.inheritancePinRecovery.decryptPin.name, + dialogs: [], + }, + { + name: lang.strings.dialogs.inheritancePinRecovery.viewPin.name, + dialogs: [], + }, + { + name: lang.strings.dialogs.inheritancePinRecovery.success.name, + dialogs: [], + dontShowOnMilestone: true, + }, + ]; + + const { + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + } = useTabsAndDialogs({ + deviceRequiredDialogsMap, + tabs, + dialogName: 'inheritancePinRecovery', + }); + + const onClose = () => { + dispatch(closeDialog('inheritancePinRecovery')); + }; + + const onHelp = () => { + dispatch(openContactSupportDialog()); + }; + + const [retryIndex, setRetryIndex] = useState(0); + const [unhandledError, setUnhandledError] = useState(); + const encryptedMessageRef = useRef(); + + const onError = useCallback((e?: any) => { + setUnhandledError(e); + }, []); + + const clearErrors = useCallback(() => { + setUnhandledError(undefined); + }, []); + + const walletAuthService = useWalletAuth(onError); + const decryptMessageService = useDecryptMessage(onError); + const sessionService = useSession(onError); + + const authTokenConfig = useMemo( + () => walletAuthService.authTokenConfig, + [walletAuthService], + ); + + const walletAuthFetchRequestId = useCallback(() => { + walletAuthService.fetchRequestId( + walletId, + InheritanceUserTypeMap.nominee, + 'wallet-based', + ); + }, [selectedWallet, walletAuthService.fetchRequestId]); + + const fetchEncryptedDataHandler = useCallback(async () => { + let sessionId = await sessionService.getIsActive(); + + if (!sessionId) { + sessionId = await sessionService.start(); + } + + assert(sessionId, 'sessionId not found'); + assert(authTokenConfig, 'accessToken not found'); + + const result = await inheritancePlanService.recover.recover({ + sessionId, + authTokenConfig, + message: true, + }); + + if (result.error) { + throw result.error; + } + + encryptedMessageRef.current = result.result.encryptedMessage; + return true; + }, [sessionService.start, sessionService.getIsActive, authTokenConfig]); + + const [ + fetchEncryptedData, + isFetchingEncryptedData, + isEncryptedDataFetched, + resetFetchEncryptedData, + ] = useAsync(fetchEncryptedDataHandler, onError); + + const startDecryption = useCallback(async () => { + assert(encryptedMessageRef.current, 'encryptedMessage not found'); + decryptMessageService.start(walletId, encryptedMessageRef.current); + }, [walletId, decryptMessageService.start]); + + const resetAll = useCallback(() => { + setRetryIndex(v => v + 1); + setUnhandledError(undefined); + encryptedMessageRef.current = undefined; + walletAuthService.reset(); + sessionService.reset(); + decryptMessageService.reset(); + resetFetchEncryptedData(); + }, [ + walletAuthService.reset, + decryptMessageService.reset, + sessionService.reset, + resetFetchEncryptedData, + ]); + + const onRetryFuncMap = useMemo< + Record boolean) | undefined> | undefined> + >(() => ({}), []); + + const onRetry = useCallback(() => { + const retryLogic = onRetryFuncMap[currentTab]?.[currentDialog]; + + if (retryLogic) { + setRetryIndex(v => v + 1); + retryLogic(); + } else { + resetAll(); + goTo(0, 0); + } + + setUnhandledError(undefined); + }, [ + currentTab, + currentDialog, + onRetryFuncMap, + walletAuthService.reset, + resetAll, + ]); + + const ctx = useMemoReturn({ + onNext, + onPrevious, + tabs, + onClose, + onHelp, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + selectedWallet, + userDetails, + walletAuthDeviceEvents: walletAuthService.deviceEvents, + walletAuthFetchRequestId, + walletAuthIsFetchingRequestId: walletAuthService.isFetchingRequestId, + walletAuthStart: walletAuthService.startWalletAuth, + walletAuthValidateSignature: walletAuthService.validateSignature, + walletAuthIsValidatingSignature: walletAuthService.isValidatingSignature, + walletAuthStep: walletAuthService.currentStep, + walletAuthAbort: walletAuthService.abortWalletAuth, + otpVerificationDetails: walletAuthService.otpVerificationDetails, + verifyOtp: walletAuthService.verifyOtp, + isVerifyingOtp: walletAuthService.isVerifyingOtp, + onRetry, + retryIndex, + unhandledError, + clearErrors, + fetchEncryptedData, + isFetchingEncryptedData, + isEncryptedDataFetched, + decryptPinStart: startDecryption, + decryptPinAbort: decryptMessageService.abort, + decryptPinDeviceEvents: decryptMessageService.deviceEvents, + decryptPinIsCompleted: decryptMessageService.isDecrypted, + }); + + return ( + + {children} + + ); +}; + +export function useInheritancePinRecoveryDialog(): InheritancePinRecoveryDialogContextInterface { + return useContext(InheritancePinRecoveryDialogContext); +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/context/types.ts b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/context/types.ts new file mode 100644 index 000000000..213246580 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/context/types.ts @@ -0,0 +1,76 @@ +import { ITabs } from '~/hooks'; + +import { WalletAuthLoginStep, IOtpVerificationDetails } from '../../hooks'; + +export interface IUserDetails { + name: string; + email: string; + alternateEmail: string; +} + +export interface InheritancePinRecoveryDialogContextInterface { + tabs: ITabs; + onNext: (tab?: number, dialog?: number) => void; + goTo: (tab: number, dialog?: number) => void; + onPrevious: () => void; + onClose: () => void; + onHelp: () => void; + currentTab: number; + currentDialog: number; + isDeviceRequired: boolean; + unhandledError?: any; + retryIndex: number; + selectedWallet?: string; + userDetails?: IUserDetails; + walletAuthDeviceEvents: Record; + walletAuthFetchRequestId: () => void; + walletAuthIsFetchingRequestId: boolean; + walletAuthStart: () => void; + walletAuthAbort: () => void; + walletAuthIsValidatingSignature: boolean; + walletAuthValidateSignature: () => Promise; + walletAuthStep: WalletAuthLoginStep; + otpVerificationDetails?: IOtpVerificationDetails; + verifyOtp: (otp: string) => Promise; + isVerifyingOtp: boolean; + clearErrors: () => void; + fetchEncryptedData: () => Promise; + isFetchingEncryptedData: boolean; + isEncryptedDataFetched: boolean; + decryptPinStart: () => void; + decryptPinAbort: () => void; + decryptPinDeviceEvents: Record; + decryptPinIsCompleted: boolean; + onRetry: () => void; +} + +export const tabIndicies = { + sync: { + tabNumber: 0, + dialogs: { + fetchRequestId: 0, + walletAuth: 1, + validateSignature: 2, + verifyOtp: 3, + fetchData: 4, + }, + }, + decrypt: { + tabNumber: 1, + dialogs: { + decrypt: 0, + }, + }, + viewPin: { + tabNumber: 2, + dialogs: { + viewPin: 0, + }, + }, + success: { + tabNumber: 3, + dialogs: { + success: 1, + }, + }, +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/index.tsx new file mode 100644 index 000000000..bf4858a6e --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PinRecovery/index.tsx @@ -0,0 +1,118 @@ +import { + BlurOverlay, + CloseButton, + DialogBox, + DialogBoxBackgroundBar, + DialogBoxBody, + HelpButton, + MilestoneAside, + WalletDialogMainContainer, +} from '@cypherock/cysync-ui'; +import React, { FC } from 'react'; + +import { + ErrorHandlerDialog, + WithConnectedDevice, + WithConnectedDeviceProps, +} from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { + InheritancePinRecoveryDialogProps, + InheritancePinRecoveryDialogProvider, + useInheritancePinRecoveryDialog, +} from './context'; + +export type { InheritancePinRecoveryDialogProps } from './context'; + +const DeviceConnectionWrapper: React.FC<{ + isDeviceRequired: boolean; + children: React.ReactNode; + withConnectedProps?: WithConnectedDeviceProps; +}> = ({ isDeviceRequired, withConnectedProps, children }) => { + if (isDeviceRequired) + return ( + + {children} + + ); + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; +}; + +DeviceConnectionWrapper.defaultProps = { + withConnectedProps: {}, +}; + +const InheritancePinRecovery: FC = () => { + const lang = useAppSelector(selectLanguage); + const { + tabs, + currentTab, + currentDialog, + unhandledError, + onClose, + onHelp, + isDeviceRequired, + onRetry, + } = useInheritancePinRecoveryDialog(); + + return ( + + + <> + !t.dontShowOnMilestone) + .map(t => t.name)} + activeTab={currentTab} + heading={lang.strings.dialogs.inheritancePinRecovery.title} + /> + + + + + {tabs[currentTab]?.dialogs[currentDialog]} + + + + onClose()} />} + leftComponent={ + + } + position="top" + useLightPadding + /> + + + + + ); +}; + +export const InheritancePinRecoveryDialog: React.FC< + InheritancePinRecoveryDialogProps +> = props => ( + + + +); diff --git a/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/FetchData.tsx b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/FetchData.tsx new file mode 100644 index 000000000..1a79881e7 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/FetchData.tsx @@ -0,0 +1,43 @@ +import { + DialogBox, + Flex, + Image, + loaderGrayIcon, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useEffect } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritancePlanLoginDialog } from '../context'; + +export const FetchData: React.FC = () => { + const lang = useAppSelector(selectLanguage); + const { fetchPlan, retryIndex } = useInheritancePlanLoginDialog(); + + const strings = lang.strings.dialogs.inheritancePlanLogin.fetchData; + + useEffect(() => { + fetchPlan(); + }, [retryIndex]); + + return ( + + Loader icon + + + {strings.title} + + + {strings.subTitle} + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/FetchRequestId.tsx b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/FetchRequestId.tsx new file mode 100644 index 000000000..d33827a12 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/FetchRequestId.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; + +import { useInheritancePlanLoginDialog } from '../context'; + +export const FetchRequestId = () => { + const { onNext, walletAuthStep, walletAuthFetchRequestId, retryIndex } = + useInheritancePlanLoginDialog(); + + useEffect(() => { + walletAuthFetchRequestId(); + }, [retryIndex]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.fetchRequestId) { + onNext(); + } + }, [walletAuthStep]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/ValidateSignature.tsx b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/ValidateSignature.tsx new file mode 100644 index 000000000..a93c1dfcb --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/ValidateSignature.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; + +import { useInheritancePlanLoginDialog } from '../context'; + +export const ValidateSignature = () => { + const { onNext, walletAuthStep, walletAuthValidateSignature, retryIndex } = + useInheritancePlanLoginDialog(); + + useEffect(() => { + walletAuthValidateSignature(); + }, [retryIndex]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.validateSignature) { + onNext(); + } + }, [walletAuthStep]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/VerifyEmail.tsx b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/VerifyEmail.tsx new file mode 100644 index 000000000..c24236df4 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/VerifyEmail.tsx @@ -0,0 +1,78 @@ +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { OTPInputDialog, OTPInputDialogRef } from '../../components'; +import { useInheritancePlanLoginDialog } from '../context'; + +export const VerifyEmail: React.FC = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.dialogs.inheritancePlanLogin; + + const { + onClose, + onPrevious, + onNext, + otpVerificationDetails, + verifyOtp, + isVerifyingOtp, + } = useInheritancePlanLoginDialog(); + + const { title } = strings.verifyEmail; + + const otpRef = useRef(null); + + const onVerify = useCallback( + async (otp: string) => { + await verifyOtp(otp); + }, + [verifyOtp], + ); + + const onResend = () => { + // DUMMY FUNCTION + }; + + const otpExpireTime = useMemo( + () => otpVerificationDetails?.otpExpiry ?? '', + [otpVerificationDetails?.otpExpiry], + ); + + const otpLength = 6; + + const retriesRemaining = otpVerificationDetails?.retriesRemaining ?? 3; + const email = otpVerificationDetails?.emails ?? ''; + + useEffect(() => { + if (otpRef.current) { + otpRef.current.reset(); + } + }, [otpVerificationDetails?.id]); + + useEffect(() => { + if (!otpVerificationDetails) onNext(); + }, [otpVerificationDetails]); + + if (!otpVerificationDetails) { + return null; + } + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/WalletAuth.tsx b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/WalletAuth.tsx new file mode 100644 index 000000000..2edfca45f --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/WalletAuth.tsx @@ -0,0 +1,143 @@ +import { InheritanceWalletAuthDeviceEvent } from '@cypherock/app-support-inheritance'; +import { + ArrowRightIcon, + Check, + DialogBox, + DialogBoxBody, + Flex, + LangDisplay, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + ScrollableContainer, + tapAnyCardDeviceAnimation2DVideo, + Throbber, + Typography, + Video, +} from '@cypherock/cysync-ui'; +import React, { useEffect } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { WalletAuthLoginStep } from '../../hooks'; +import { useInheritancePlanLoginDialog } from '../context'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const WalletAuth: React.FC = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.dialogs.inheritancePlanLogin; + + const { + onClose, + onNext, + walletAuthDeviceEvents, + walletAuthStep, + walletAuthStart, + walletAuthAbort, + retryIndex, + clearErrors, + walletName, + } = useInheritancePlanLoginDialog(); + + const getDeviceEventIcon = ( + loadingEvent: InheritanceWalletAuthDeviceEvent, + completedEvent: InheritanceWalletAuthDeviceEvent, + ) => { + if (walletAuthDeviceEvents[completedEvent]) return checkIconComponent; + if (walletAuthDeviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.walletAuth.actions.confirm, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceWalletAuthDeviceEvent.INIT, + InheritanceWalletAuthDeviceEvent.CONFIRMED, + ), + }, + { + id: '2', + text: strings.walletAuth.actions.tapCard, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceWalletAuthDeviceEvent.CONFIRMED, + InheritanceWalletAuthDeviceEvent.WALLET_BASED_CARD_TAPPED, + ), + }, + ]; + + return actions; + }, [strings, walletAuthDeviceEvents]); + + useEffect(() => { + clearErrors(); + walletAuthStart(); + + return () => { + walletAuthAbort(); + }; + }, [retryIndex, clearErrors]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.walletAuth) { + onNext(); + } + }, [walletAuthStep]); + + return ( + + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/index.tsx new file mode 100644 index 000000000..9c2adfcc3 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/Dialogs/index.tsx @@ -0,0 +1,5 @@ +export * from './FetchRequestId'; +export * from './WalletAuth'; +export * from './ValidateSignature'; +export * from './VerifyEmail'; +export * from './FetchData'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/context/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/context/index.tsx new file mode 100644 index 000000000..12a67a01d --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/context/index.tsx @@ -0,0 +1,241 @@ +import React, { + Context, + FC, + ReactNode, + createContext, + useCallback, + useContext, + useMemo, + useState, +} from 'react'; + +import { ITabs, useAsync, useMemoReturn, useTabsAndDialogs } from '~/hooks'; +import { InheritanceUserTypeMap } from '~/services'; +import { closeDialog, useAppDispatch } from '~/store'; + +import { + InheritancePlanLoginDialogContextInterface, + InheritancePlanLoginDialogProps, + tabIndicies, +} from './types'; + +import { useWalletAuth, WalletAuthLoginStep } from '../../hooks'; +import { + WalletAuth, + VerifyEmail, + FetchData, + FetchRequestId, + ValidateSignature, +} from '../Dialogs'; +import { useUpdatePlanDetails } from '../../hooks/useUpdatePlanDetails'; + +export const InheritancePlanLoginDialogContext: Context = + createContext( + {} as InheritancePlanLoginDialogContextInterface, + ); + +export interface InheritancePlanLoginDialogContextProviderProps + extends InheritancePlanLoginDialogProps { + children: ReactNode; +} + +export const InheritancePlanLoginDialogProvider: FC< + InheritancePlanLoginDialogContextProviderProps +> = ({ children, walletId, walletName }) => { + const dispatch = useAppDispatch(); + + const deviceRequiredDialogsMap: Record = + useMemo( + () => ({ + [tabIndicies.walletAuth.tabNumber]: [ + tabIndicies.walletAuth.dialogs.walletAuth, + ], + }), + [], + ); + + const tabs: ITabs = useMemo( + () => [ + { + name: 'Fetch Request Id', + dialogs: [], + }, + { + name: 'Wallet Auth', + dialogs: [], + }, + { + name: 'Validate Signature', + dialogs: [], + }, + { + name: 'Verify Email', + dialogs: [], + }, + { + name: 'Fetch Data', + dialogs: [], + }, + ], + [], + ); + + const { + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + } = useTabsAndDialogs({ + deviceRequiredDialogsMap, + tabs, + dialogName: 'inheritancePlanLogin', + }); + + const onClose = () => { + dispatch(closeDialog('inheritancePlanLogin')); + }; + + const [unhandledError, setUnhandledError] = useState(); + const [retryIndex, setRetryIndex] = useState(0); + + const onError = useCallback((e?: any) => { + setUnhandledError(e); + }, []); + + const clearErrors = useCallback(() => { + setUnhandledError(undefined); + }, []); + + const walletAuthService = useWalletAuth(onError); + const authTokenConfig = useMemo( + () => walletAuthService.authTokenConfig, + [walletAuthService], + ); + + const walletAuthFetchRequestId = useCallback(() => { + walletAuthService.fetchRequestId( + walletId, + InheritanceUserTypeMap.owner, + 'seed-based', + ); + }, [walletId]); + + const { fetchAndUpdatePlan } = useUpdatePlanDetails({ authTokenConfig }); + + const fetchPlanHanlder = useCallback(async () => { + if (!authTokenConfig) return false; + await fetchAndUpdatePlan(); + onClose(); + return true; + }, [authTokenConfig, onClose]); + + const [fetchPlan, isFetchingPlan] = useAsync(fetchPlanHanlder, onError); + + const onRetryFuncMap = useMemo< + Record boolean) | undefined> | undefined> + >( + () => ({ + [tabIndicies.fetchRequestId.tabNumber]: { + [tabIndicies.fetchRequestId.dialogs.fetchRequestId]: () => true, + }, + [tabIndicies.walletAuth.tabNumber]: { + [tabIndicies.walletAuth.dialogs.walletAuth]: () => true, + }, + [tabIndicies.validateSignature.tabNumber]: { + [tabIndicies.validateSignature.dialogs.validateSignature]: () => true, + }, + }), + [], + ); + + const onRetry = useCallback(() => { + const retryLogic = onRetryFuncMap[currentTab]?.[currentDialog]; + + if (retryLogic) { + setRetryIndex(v => v + 1); + retryLogic(); + } else { + setRetryIndex(v => v + 1); + walletAuthService.reset(); + goTo(0, 0); + } + + setUnhandledError(undefined); + }, [currentTab, currentDialog, onRetryFuncMap, walletAuthService.reset]); + + const onNextActionMapPerDialog = useMemo< + Record boolean) | undefined> | undefined> + >( + () => ({ + [tabIndicies.fetchRequestId.tabNumber]: { + [tabIndicies.fetchRequestId.dialogs.fetchRequestId]: () => { + if (walletAuthService.currentStep === WalletAuthLoginStep.completed) { + goTo( + tabIndicies.fetchData.tabNumber, + tabIndicies.fetchData.dialogs.fetchData, + ); + return true; + } + + return false; + }, + }, + }), + [walletAuthService.currentStep], + ); + + const onNextCallback = useCallback(() => { + const action = onNextActionMapPerDialog[currentTab]?.[currentDialog]; + let doNext = true; + + if (action) { + doNext = !action(); + } + + if (doNext) { + onNext(); + } + }, [onNext, currentTab, currentDialog, onNextActionMapPerDialog]); + + const ctx = useMemoReturn({ + onNext: onNextCallback, + onPrevious, + tabs, + onClose, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + unhandledError, + onRetry, + retryIndex, + clearErrors, + walletName, + walletAuthDeviceEvents: walletAuthService.deviceEvents, + walletAuthFetchRequestId, + walletAuthIsFetchingRequestId: walletAuthService.isFetchingRequestId, + walletAuthStart: walletAuthService.startWalletAuth, + walletAuthValidateSignature: walletAuthService.validateSignature, + walletAuthIsValidatingSignature: walletAuthService.isValidatingSignature, + walletAuthStep: walletAuthService.currentStep, + walletAuthAbort: walletAuthService.abortWalletAuth, + isRegisteringUser: walletAuthService.isRegisteringUser, + otpVerificationDetails: walletAuthService.otpVerificationDetails, + verifyOtp: walletAuthService.verifyOtp, + isVerifyingOtp: walletAuthService.isVerifyingOtp, + fetchPlan, + isFetchingPlan, + }); + + return ( + + {children} + + ); +}; + +export function useInheritancePlanLoginDialog(): InheritancePlanLoginDialogContextInterface { + return useContext(InheritancePlanLoginDialogContext); +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/context/types.tsx b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/context/types.tsx new file mode 100644 index 000000000..22cb6a5a7 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/context/types.tsx @@ -0,0 +1,70 @@ +import { ITabs } from '~/hooks'; + +import { WalletAuthLoginStep, IOtpVerificationDetails } from '../../hooks'; + +export interface InheritancePlanLoginDialogProps { + walletId: string; + walletName: string; +} + +export interface InheritancePlanLoginDialogContextInterface { + tabs: ITabs; + onNext: (tab?: number, dialog?: number) => void; + goTo: (tab: number, dialog?: number) => void; + onPrevious: () => void; + currentTab: number; + currentDialog: number; + isDeviceRequired: boolean; + onClose: () => void; + unhandledError?: any; + onRetry: () => void; + retryIndex: number; + clearErrors: () => void; + walletName: string; + walletAuthDeviceEvents: Record; + walletAuthFetchRequestId: () => void; + walletAuthIsFetchingRequestId: boolean; + walletAuthStart: () => void; + walletAuthAbort: () => void; + walletAuthIsValidatingSignature: boolean; + walletAuthValidateSignature: () => Promise; + walletAuthStep: WalletAuthLoginStep; + otpVerificationDetails?: IOtpVerificationDetails; + verifyOtp: (otp: string) => Promise; + isVerifyingOtp: boolean; + fetchPlan: () => void; + isFetchingPlan: boolean; +} + +export const tabIndicies = { + fetchRequestId: { + tabNumber: 0, + dialogs: { + fetchRequestId: 0, + }, + }, + walletAuth: { + tabNumber: 1, + dialogs: { + walletAuth: 0, + }, + }, + validateSignature: { + tabNumber: 2, + dialogs: { + validateSignature: 0, + }, + }, + verifyEmail: { + tabNumber: 3, + dialogs: { + verifyEmail: 0, + }, + }, + fetchData: { + tabNumber: 4, + dialogs: { + fetchData: 0, + }, + }, +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/index.tsx new file mode 100644 index 000000000..f04f44db9 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/PlanLogin/index.tsx @@ -0,0 +1,76 @@ +import { BlurOverlay } from '@cypherock/cysync-ui'; +import React, { FC } from 'react'; + +import { + ErrorHandlerDialog, + WithConnectedDevice, + WithConnectedDeviceProps, +} from '~/components'; + +import { + InheritancePlanLoginDialogProvider, + useInheritancePlanLoginDialog, +} from './context'; +import { InheritancePlanLoginDialogProps } from './context/types'; + +export * from './context/types'; + +const DeviceConnectionWrapper: React.FC<{ + isDeviceRequired: boolean; + children: React.ReactNode; + withConnectedProps?: WithConnectedDeviceProps; +}> = ({ isDeviceRequired, withConnectedProps, children }) => { + if (isDeviceRequired) + return ( + + {children} + + ); + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; +}; + +DeviceConnectionWrapper.defaultProps = { + withConnectedProps: {}, +}; + +const InheritancePlanLogin: FC = () => { + const { + tabs, + currentTab, + currentDialog, + unhandledError, + onClose, + onRetry, + isDeviceRequired, + } = useInheritancePlanLoginDialog(); + + return ( + + + + {tabs[currentTab]?.dialogs[currentDialog]} + + + + ); +}; + +export const InheritancePlanLoginDialog: React.FC< + InheritancePlanLoginDialogProps +> = props => ( + + + +); diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Checkout.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Checkout.tsx new file mode 100644 index 000000000..8265dcda9 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Checkout.tsx @@ -0,0 +1,94 @@ +import { + Button, + Container, + LangDisplay, + Payment, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useState } from 'react'; + +import { constants } from '~/constants'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Checkout = () => { + const [tempCoupon, setCoupon] = useState(''); + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceSilverPlanPurchase.checkout; + const [isValidCoupon, setIsValidCoupon] = useState(false); + const [isPlanActivated, setIsPlanActivated] = useState(false); + + const { + onNext, + applyCoupon, + removeCoupon, + isApplyingCoupon, + applyingCouponError, + couponDuration, + isActivatingCoupon, + activateCoupon, + } = useInheritanceSilverPlanPurchaseDialog(); + + const onApply = async () => { + const isValid = await applyCoupon(tempCoupon); + setIsValidCoupon(isValid); + }; + + const onActivate = async () => { + const isActivated = await activateCoupon(); + setIsPlanActivated(isActivated); + }; + + const onDelete = () => { + setCoupon(''); + removeCoupon(); + setIsValidCoupon(false); + setIsPlanActivated(false); + }; + + const couponText = tempCoupon; + + useEffect(() => { + if (isPlanActivated) { + onNext(); + } + }, [isPlanActivated, onNext]); + + return ( + + {lang.strings.buttons.confirm} + + } + > + + + + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Email/UserDetails.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Email/UserDetails.tsx new file mode 100644 index 000000000..eb822475b --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Email/UserDetails.tsx @@ -0,0 +1,106 @@ +import { Button, LangDisplay } from '@cypherock/cysync-ui'; +import React, { useEffect, useMemo, useState } from 'react'; +import { UserDetailsForm } from '../../../components'; + +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +export const UserDetails = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritance.dialog.userDetails; + const silverPlanStrings = + lang.strings.inheritanceSilverPlanPurchase.email.userDetails; + + const { + registerUser, + onPrevious, + onNext, + isRegisteringUser, + walletAuthStep, + userDetailPrefillData, + } = useInheritanceSilverPlanPurchaseDialog(); + + const [name, setName] = useState(userDetailPrefillData.name); + const [email, setEmail] = useState(userDetailPrefillData.email); + const [alternateEmail, setAlternateEmail] = useState( + userDetailPrefillData.alternateEmail, + ); + + const formId = 'inheritance-silver-plan-user-details'; + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (isRegisteringUser) return; + + registerUser({ + name, + email, + alternateEmail, + }); + }; + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.userDetails) { + onNext(); + } + }, []); + + const [hasErrors, setHasErrors] = useState(false); + const isSameEmail = useMemo( + () => Boolean(email && email === alternateEmail), + [email, alternateEmail], + ); + const isFormIncomplete = useMemo( + () => !name || !email || !alternateEmail || hasErrors, + [hasErrors, name, email, alternateEmail], + ); + + return ( + + + + + } + > + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Email/VerifyOTP.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Email/VerifyOTP.tsx new file mode 100644 index 000000000..06f1f5dba --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Email/VerifyOTP.tsx @@ -0,0 +1,92 @@ +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; + +import { + OTPInputDialog, + OTPInputDialogRef, +} from '~/dialogs/Inheritance/components'; +import { OtpVerificationConcern } from '~/dialogs/Inheritance/hooks'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../../context'; + +export const VerifyOTP: React.FC = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritance.dialog.verifyOTP; + + const { + onPrevious, + onNext, + otpVerificationDetails, + verifyOtp, + isVerifyingOtp, + onClose, + } = useInheritanceSilverPlanPurchaseDialog(); + + const title = useMemo(() => { + const map: Record = { + [OtpVerificationConcern.primary]: strings.primaryEmailOTP.title, + [OtpVerificationConcern.alternate]: strings.alternateEmailOTP.title, + [OtpVerificationConcern.login]: + lang.strings.dialogs.inheritancePlanLogin.verifyEmail.title, + }; + + if (!otpVerificationDetails) return ''; + + return map[otpVerificationDetails.concern]; + }, [otpVerificationDetails?.concern, lang]); + + const otpRef = useRef(null); + + const onVerify = useCallback( + async (otp: string) => { + await verifyOtp(otp); + }, + [verifyOtp], + ); + + const onResend = () => { + // DUMMY FUNCTION + }; + + const otpExpireTime = useMemo( + () => otpVerificationDetails?.otpExpiry ?? '', + [otpVerificationDetails?.otpExpiry], + ); + + const otpLength = 6; + + const retriesRemaining = otpVerificationDetails?.retriesRemaining ?? 3; + const email = otpVerificationDetails?.emails ?? ''; + + useEffect(() => { + if (otpRef.current) { + otpRef.current.reset(); + } + }, [otpVerificationDetails?.id]); + + useEffect(() => { + if (!otpVerificationDetails) onNext(); + }, [otpVerificationDetails]); + + if (!otpVerificationDetails) { + return null; + } + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Email/index.ts b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Email/index.ts new file mode 100644 index 000000000..549cfb854 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Email/index.ts @@ -0,0 +1,2 @@ +export * from './UserDetails'; +export * from './VerifyOTP'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/Device.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/Device.tsx new file mode 100644 index 000000000..75a190084 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/Device.tsx @@ -0,0 +1,163 @@ +import { InheritanceEncryptMessageDeviceEvent } from '@cypherock/app-support-inheritance'; +import { + ArrowRightIcon, + CardTapList, + Check, + Container, + LangDisplay, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + QuestionMarkButton, + tapAnyCardDeviceAnimation2DVideo, + Throbber, + Tooltip, + Typography, + Video, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useState } from 'react'; + +import { LoaderDialog } from '~/components'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const DeviceEncryption = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritanceSilverPlanPurchase.encryption.device; + const [cardTapState, setCardTapState] = useState(-1); + + const { + onNext, + encryptPinDeviceEvents, + encryptPinAbort, + encryptPinStart, + clearErrors, + retryIndex, + encryptPinIsCompleted, + isEstablishingSession, + } = useInheritanceSilverPlanPurchaseDialog(); + + const getDeviceEventIcon = ( + loadingEvent: InheritanceEncryptMessageDeviceEvent, + completedEvent: InheritanceEncryptMessageDeviceEvent, + ) => { + if (encryptPinDeviceEvents[completedEvent]) return checkIconComponent; + if (encryptPinDeviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.actions.confirm, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceEncryptMessageDeviceEvent.INIT, + InheritanceEncryptMessageDeviceEvent.CONFIRMED, + ), + }, + ]; + + return actions; + }, [encryptPinDeviceEvents]); + + useEffect(() => { + const eventToState: Record = { + [InheritanceEncryptMessageDeviceEvent.CONFIRMED]: 0, + [InheritanceEncryptMessageDeviceEvent.PIN_CARD_TAPPED]: 1, + [InheritanceEncryptMessageDeviceEvent.MESSAGE_CARD_TAPPED]: 2, + }; + + let state: number | undefined; + for (const event in eventToState) { + if (encryptPinDeviceEvents[event] && eventToState[event] !== undefined) { + state = eventToState[event]!; + } + } + + if (state !== undefined) { + setCardTapState(state); + } + }, [encryptPinDeviceEvents]); + + useEffect(() => { + clearErrors(); + encryptPinStart(); + setCardTapState(-1); + + return () => { + encryptPinAbort(); + }; + }, [retryIndex, clearErrors]); + + useEffect(() => { + if (encryptPinIsCompleted) { + onNext(); + } + }, [encryptPinIsCompleted]); + + if (isEstablishingSession) { + return ; + } + + return ( + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/Loader.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/Loader.tsx new file mode 100644 index 000000000..81d080db5 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/Loader.tsx @@ -0,0 +1,34 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { useCallbackAfterCountdown } from '~/hooks'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../../context'; + +export const EncryptionLoader: React.FC = () => { + const lang = useAppSelector(selectLanguage); + const { onNext, setupPlan, isSetupPlanCompleted, retryIndex } = + useInheritanceSilverPlanPurchaseDialog(); + + const strings = lang.strings.inheritanceSilverPlanPurchase.encryption.loading; + + const onNextCallback = useCallbackAfterCountdown(onNext, 3000); + + useEffect(() => { + onNextCallback.setStartTime(Date.now()); + setupPlan(); + }, [retryIndex]); + + useEffect(() => { + if (isSetupPlanCompleted) { + onNextCallback.start(); + } + + return () => { + onNextCallback.stop(); + }; + }, [isSetupPlanCompleted]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/Success.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/Success.tsx new file mode 100644 index 000000000..e84b7f002 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/Success.tsx @@ -0,0 +1,29 @@ +import { SuccessDialog } from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../../context'; + +export const EncryptionSuccess: React.FC = () => { + const { onClose, onNext } = useInheritanceSilverPlanPurchaseDialog(); + + const lang = useAppSelector(selectLanguage); + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/index.tsx new file mode 100644 index 000000000..95ba8a4ea --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Encryption/index.tsx @@ -0,0 +1,3 @@ +export * from './Device'; +export * from './Loader'; +export * from './Success'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Ensure.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Ensure.tsx new file mode 100644 index 000000000..22a38e504 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Ensure.tsx @@ -0,0 +1,59 @@ +import { + Button, + Container, + LangDisplay, + Typography, + ListContainer, + ListItem, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Ensure = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritanceSilverPlanPurchase; + + const { onNext } = useInheritanceSilverPlanPurchaseDialog(); + + return ( + onNext()} variant="primary"> + + + } + > + + + + + + {strings.instructions.ensure.instructions.map((listItem, index) => ( + + + + + + ))} + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Instructions.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Instructions.tsx new file mode 100644 index 000000000..272ff667c --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Instructions.tsx @@ -0,0 +1,49 @@ +import { constants } from '@cypherock/cysync-core-constants'; +import { Button, LangDisplay, Typography } from '@cypherock/cysync-ui'; +import React from 'react'; + +import { VideoPlayer } from '~/components/VideoPlayer'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Instructions = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceSilverPlanPurchase; + + const { onNext, onPrevious, onRetry, retryIndex } = + useInheritanceSilverPlanPurchaseDialog(); + + return ( + + + + + } + > + + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/SelectWallet.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/SelectWallet.tsx new file mode 100644 index 000000000..0c488093f --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/SelectWallet.tsx @@ -0,0 +1,141 @@ +import { + Button, + Container, + LangDisplay, + ManyInMany, + ScrollableContainer, + Tooltip, + TooltipPlacement, + Typography, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useRef, useState } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../context'; +import { Layout } from '../Layout'; + +export const SelectWallet = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceSilverPlanPurchase; + + const { onNext, onPrevious, allWallets, selectedWallet, setSelectedWallet } = + useInheritanceSilverPlanPurchaseDialog(); + + const [tooltipPlacements, setTooltipPlacements] = useState< + Record + >({}); + + const observerRef = useRef(); + + const handleObserver = (entries: IntersectionObserverEntry[]) => { + entries.forEach((entry: IntersectionObserverEntry) => { + const walletId = entry.target.getAttribute('data-wallet-id'); + if (walletId) { + let placement: TooltipPlacement; + if (entry.isIntersecting) placement = 'bottom'; + else if ( + entry.rootBounds && + entry.boundingClientRect.bottom > entry.rootBounds.bottom + ) + placement = 'top'; + else placement = 'bottom'; + setTooltipPlacements(prev => ({ ...prev, [walletId]: placement })); + } + }); + }; + + useEffect(() => { + observerRef.current = new IntersectionObserver(handleObserver, { + root: document.querySelector('.scrollable-container'), + threshold: 0.95, + }); + const observedElements = document.querySelectorAll('.wallet-card'); + + observedElements.forEach(el => { + observerRef.current?.observe(el); + }); + + return () => { + observedElements.forEach(el => { + observerRef.current?.unobserve(el); + }); + }; + }, [allWallets]); + + return ( + + + + + } + > + + + + + + + + + + + {allWallets.map(wallet => { + const isDisabled = Boolean(wallet.isDeleted) || !wallet.hasPin; + const walletId = wallet.__id ?? ''; + const tooltipPlacement = tooltipPlacements[walletId] || 'bottom'; + if (isDisabled) { + return ( + +
+ setSelectedWallet(wallet)} + $width={340} + $height={128} + /> +
+
+ ); + } + return ( + { + setSelectedWallet(wallet); + }} + $width={340} + $height={128} + /> + ); + })} +
+
+
+ ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Success.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Success.tsx new file mode 100644 index 000000000..a4023a2e0 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Success.tsx @@ -0,0 +1,27 @@ +import { SuccessDialog, Image, silverTickIcon } from '@cypherock/cysync-ui'; +import React from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../context'; + +export const Success = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritanceSilverPlanPurchase.checkout.success; + + const { onClose } = useInheritanceSilverPlanPurchaseDialog(); + + return ( + } + withConfetti + width={800} + headerType="h5" + bodyBottomPadding={4} + /> + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Terms.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Terms.tsx new file mode 100644 index 000000000..7a91eba4b --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/Terms.tsx @@ -0,0 +1,62 @@ +import { + Button, + CheckBox, + Flex, + LangDisplay, + Typography, + ExternalLink, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +import { constants } from '~/constants'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../context'; +import { Layout } from '../Layout'; + +export const Terms = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritance.termsOfService; + + const { onNext, isTermsAccepted, setIsTermsAccepted } = + useInheritanceSilverPlanPurchaseDialog(); + + const toNextPage = async () => { + if (isTermsAccepted) onNext(); + }; + + return ( + toNextPage()} + > + + + } + > + + + + + + + + setIsTermsAccepted(!isTermsAccepted)} + id="privacy_policy_accepted" + label={strings.checkBoxLabel} + /> + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/FetchRequestId.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/FetchRequestId.tsx new file mode 100644 index 000000000..9243950d9 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/FetchRequestId.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../../context'; + +export const FetchRequestId = () => { + const { onNext, walletAuthStep, walletAuthFetchRequestId, retryIndex } = + useInheritanceSilverPlanPurchaseDialog(); + + useEffect(() => { + walletAuthFetchRequestId(); + }, [retryIndex]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.fetchRequestId) { + onNext(); + } + }, [walletAuthStep]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/ValidateSignature.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/ValidateSignature.tsx new file mode 100644 index 000000000..6e3b861be --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/ValidateSignature.tsx @@ -0,0 +1,23 @@ +import React, { useEffect } from 'react'; + +import { LoaderDialog } from '~/components'; +import { WalletAuthLoginStep } from '~/dialogs/Inheritance/hooks'; + +import { useInheritanceSilverPlanPurchaseDialog } from '../../context'; + +export const ValidateSignature = () => { + const { onNext, walletAuthStep, walletAuthValidateSignature, retryIndex } = + useInheritanceSilverPlanPurchaseDialog(); + + useEffect(() => { + walletAuthValidateSignature(); + }, [retryIndex]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.validateSignature) { + onNext(); + } + }, [walletAuthStep]); + + return ; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/WalletAuth.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/WalletAuth.tsx new file mode 100644 index 000000000..eca6ff2cf --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/WalletAuth.tsx @@ -0,0 +1,176 @@ +import { InheritanceWalletAuthDeviceEvent } from '@cypherock/app-support-inheritance'; +import { + ArrowRightIcon, + CardTapList, + Check, + Container, + LangDisplay, + LeanBox, + LeanBoxContainer, + LeanBoxProps, + MessageBox, + tapAnyCardDeviceAnimation2DVideo, + Throbber, + Typography, + Video, +} from '@cypherock/cysync-ui'; +import React, { useEffect, useState } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { WalletAuthLoginStep } from '../../../hooks'; +import { useInheritanceSilverPlanPurchaseDialog } from '../../context'; +import { Layout } from '../../Layout'; + +const checkIconComponent = ; +const throbberComponent = ; +const rightArrowIcon = ; + +export const WalletAuth = () => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.inheritanceSilverPlanPurchase; + const [cardTapState, setCardTapState] = useState(-1); + + const { + onNext, + selectedWallet, + walletAuthDeviceEvents, + walletAuthStep, + walletAuthStart, + walletAuthAbort, + retryIndex, + clearErrors, + isRegisterationRequired, + } = useInheritanceSilverPlanPurchaseDialog(); + + const getDeviceEventIcon = ( + loadingEvent: InheritanceWalletAuthDeviceEvent, + completedEvent: InheritanceWalletAuthDeviceEvent, + ) => { + if (walletAuthDeviceEvents[completedEvent]) return checkIconComponent; + if (walletAuthDeviceEvents[loadingEvent]) return throbberComponent; + + return undefined; + }; + + const actionsList = React.useMemo(() => { + const actions: LeanBoxProps[] = [ + { + id: '1', + text: strings.wallet.walletAuth.actions.confirm, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceWalletAuthDeviceEvent.INIT, + InheritanceWalletAuthDeviceEvent.CONFIRMED, + ), + }, + ]; + + if (!isRegisterationRequired) { + actions.push({ + id: '2', + text: strings.wallet.walletAuth.actions.tapCard, + leftImage: rightArrowIcon, + rightImage: getDeviceEventIcon( + InheritanceWalletAuthDeviceEvent.CONFIRMED, + InheritanceWalletAuthDeviceEvent.WALLET_BASED_CARD_TAPPED, + ), + }); + } + + return actions; + }, [strings, walletAuthDeviceEvents, isRegisterationRequired]); + + useEffect(() => { + const eventToState: Record = { + [InheritanceWalletAuthDeviceEvent.CONFIRMED]: 0, + [InheritanceWalletAuthDeviceEvent.SEED_BASED_CARD_TAPPED]: 1, + [InheritanceWalletAuthDeviceEvent.CARD_PAIRING_CARD_TAPPED]: 2, + [InheritanceWalletAuthDeviceEvent.WALLET_BASED_CARD_TAPPED]: 3, + }; + + let state: number | undefined; + for (const event in eventToState) { + if (walletAuthDeviceEvents[event] && eventToState[event] !== undefined) { + state = eventToState[event]!; + } + } + + if (state !== undefined) { + setCardTapState(state); + } + }, [walletAuthDeviceEvents]); + + useEffect(() => { + clearErrors(); + walletAuthStart(); + setCardTapState(-1); + + return () => { + walletAuthAbort(); + }; + }, [retryIndex, clearErrors]); + + useEffect(() => { + if (walletAuthStep > WalletAuthLoginStep.walletAuth) { + onNext(); + } + }, [walletAuthStep]); + + return ( + + + + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/index.tsx new file mode 100644 index 000000000..b4ec4f637 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/WalletAuth/index.tsx @@ -0,0 +1,3 @@ +export * from './FetchRequestId'; +export * from './WalletAuth'; +export * from './ValidateSignature'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/index.tsx new file mode 100644 index 000000000..6c1b6b45d --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Dialogs/index.tsx @@ -0,0 +1,9 @@ +export * from './Terms'; +export * from './Ensure'; +export * from './Instructions'; +export * from './SelectWallet'; +export * from './WalletAuth'; +export * from './Email'; +export * from './Encryption'; +export * from './Checkout'; +export * from './Success'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Layout.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Layout.tsx new file mode 100644 index 000000000..eb50005af --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/Layout.tsx @@ -0,0 +1,31 @@ +import { + DialogBox, + DialogBoxBody, + DialogBoxFooter, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +export interface LayoutProps { + children: React.ReactNode; + footerComponent?: React.ReactNode; +} + +export const Layout: React.FC = ({ + children, + footerComponent, +}) => ( + + + {children} + + {footerComponent && ( + + {footerComponent} + + )} + +); + +Layout.defaultProps = { + footerComponent: undefined, +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/context/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/context/index.tsx new file mode 100644 index 000000000..9b2ece526 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/context/index.tsx @@ -0,0 +1,504 @@ +import { insertInheritancePlan } from '@cypherock/cysync-core-services'; +import { IInheritancePlan, IWallet } from '@cypherock/db-interfaces'; +import React, { + Context, + FC, + ReactNode, + createContext, + useCallback, + useContext, + useMemo, + useRef, + useState, +} from 'react'; + +import { routes } from '~/constants'; +import { + useAsync, + useMemoReturn, + useNavigateTo, + useStateWithRef, +} from '~/hooks'; +import { inheritancePlanService, InheritanceUserTypeMap } from '~/services'; +import { selectLanguage, useAppSelector } from '~/store'; +import { getDB } from '~/utils'; + +import { + InheritanceSilverPlanPurchaseDialogContextInterface, + IWalletForSelection, +} from './types'; +import { tabIndicies, useSilverPlanDialogHanlders } from './useDialogHandler'; + +import { + IUserDetails, + useEncryptMessage, + useSession, + useWalletAuth, + WalletAuthLoginStep, +} from '../../hooks'; + +export * from './types'; + +export const InheritanceSilverPlanPurchaseDialogContext: Context = + createContext( + {} as InheritanceSilverPlanPurchaseDialogContextInterface, + ); + +export interface InheritanceSilverPlanPurchaseDialogContextProviderProps { + children: ReactNode; +} + +export const InheritanceSilverPlanPurchaseDialogProvider: FC< + InheritanceSilverPlanPurchaseDialogContextProviderProps +> = ({ children }) => { + const { + onClose, + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + tabs, + } = useSilverPlanDialogHanlders(); + + const wallets = useAppSelector(state => state.wallet.wallets); + const lang = useAppSelector(selectLanguage); + const deletedWallets = useAppSelector(state => state.wallet.deletedWallets); + const plans = useAppSelector(state => state.inheritance.inheritancePlans); + const navigateTo = useNavigateTo(); + + const isPlanActive = (plan: IInheritancePlan, walletId: string) => { + const now = Date.now(); + return ( + plan.walletId === walletId && + plan.purchasedAt && + plan.purchasedAt <= now && + plan.expireAt && + plan.expireAt >= now + ); + }; + + const allWallets = useMemo(() => { + const deletedWalletIds = deletedWallets.map(e => e.__id); + + return [ + ...wallets.map(e => ({ + ...e, + isDeleted: deletedWalletIds.includes(e.__id), + isActive: plans.some(plan => e.__id && isPlanActive(plan, e.__id)), + })), + ]; + }, [wallets, deletedWallets, plans]); + + const [isTermsAccepted, setIsTermsAccepted] = useState(false); + const [selectedWallet, setSelectedWallet] = useState(); + const [retryIndex, setRetryIndex] = useState(0); + const [unhandledError, setUnhandledError] = useState(); + const [coupon, setCoupon, couponRef] = useStateWithRef(''); + const [applyingCouponError, setApplyingCouponError] = useState< + { heading: string; subtext: string } | undefined + >(); + const [userDetailPrefillData, setUserDetailPrefillData] = + useState({ + name: '', + email: '', + alternateEmail: '', + }); + const [couponDuration, setCouponDuration] = useState(''); + + const onError = useCallback((e?: any) => { + setUnhandledError(e); + }, []); + + const clearErrors = useCallback(() => { + setUnhandledError(undefined); + }, []); + + const walletAuthService = useWalletAuth(onError, setUserDetailPrefillData); + const encryptMessageService = useEncryptMessage(onError); + const sessionService = useSession(onError); + const sessionIdRef = useRef(); + + const authTokenConfig = useMemo( + () => walletAuthService.authTokenConfig, + [walletAuthService], + ); + + const walletAuthFetchRequestId = useCallback(() => { + if (!selectedWallet?.__id) { + return; + } + + walletAuthService.fetchRequestId( + selectedWallet.__id, + InheritanceUserTypeMap.owner, + 'seed-based', + true, + ); + }, [selectedWallet, walletAuthService.fetchRequestId]); + + const encryptPinStart = useCallback(async () => { + if (!selectedWallet?.__id) { + return; + } + + let sessionId = await sessionService.getIsActive(); + + if (!sessionId) { + sessionId = await sessionService.start(); + } + + sessionIdRef.current = sessionId; + + if (sessionId) { + encryptMessageService.start(selectedWallet.__id); + } + }, [ + selectedWallet, + encryptMessageService.start, + sessionService.start, + sessionService.getIsActive, + sessionService.sessionId, + ]); + + const onRetryFuncMap = useMemo< + Record boolean) | undefined> | undefined> + >( + () => ({ + [tabIndicies.instructions.tabNumber]: { + [tabIndicies.instructions.dialogs.video]: () => true, + }, + [tabIndicies.wallet.tabNumber]: { + [tabIndicies.wallet.dialogs.fetchRequestId]: () => true, + [tabIndicies.wallet.dialogs.walletAuth]: () => true, + [tabIndicies.wallet.dialogs.validateSignature]: () => true, + }, + [tabIndicies.encryption.tabNumber]: { + [tabIndicies.encryption.dialogs.deviceEncryption]: () => true, + [tabIndicies.encryption.dialogs.encryptionLoader]: () => true, + }, + }), + [], + ); + + const setupPlanHandler = useCallback(async () => { + if ( + !encryptMessageService.encryptedMessages || + !authTokenConfig || + !selectedWallet || + !sessionIdRef.current + ) + return false; + + const result = await inheritancePlanService.create({ + encryptedData: encryptMessageService.encryptedMessages, + sessionId: sessionIdRef.current, + authTokenConfig, + }); + + if (result.error) { + throw result.error; + } + + const db = getDB(); + await insertInheritancePlan(db, { + walletId: selectedWallet.__id ?? '', + isNominee: false, + type: 'silver', + walletName: selectedWallet.name, + }); + + return true; + }, [ + encryptMessageService.encryptedMessages, + authTokenConfig, + selectedWallet, + ]); + + const [setupPlan, isSettingUpPlan, isSetupPlanCompleted, resetSetupPlan] = + useAsync(setupPlanHandler, onError); + + const applyCouponHandler = useCallback( + async (_coupon: string) => { + setApplyingCouponError(undefined); + + if (!authTokenConfig) return false; + + try { + const result = await inheritancePlanService.checkCoupon({ + coupon: _coupon, + authTokenConfig, + }); + + if (result.error) throw result.error; + + setCouponDuration(result.result?.duration ?? ''); + setCoupon(_coupon); + } catch (error) { + setApplyingCouponError({ + heading: lang.strings.inheritance.dialog.payment.error.errorHeading, + subtext: lang.strings.inheritance.dialog.payment.error.subtext, + }); + return false; + } + + return true; + }, + [authTokenConfig, onNext], + ); + + const [applyCoupon, isApplyingCoupon, isCouponApplied, resetApplyCoupon] = + useAsync(applyCouponHandler, onError); + + const activateCouponHandler = useCallback(async () => { + if (!authTokenConfig || !couponRef.current || !selectedWallet) return false; + + const result = await inheritancePlanService.activate({ + coupon: couponRef.current, + authTokenConfig, + }); + + if (result.error) { + throw result.error; + } + + const db = getDB(); + await insertInheritancePlan(db, { + walletId: selectedWallet.__id ?? '', + isNominee: false, + type: 'silver', + walletName: selectedWallet.name, + purchasedAt: Date.now(), + expireAt: + Date.now() + + 1000 * 60 * 60 * 24 * 365 * parseInt(couponDuration.split(' ')[0], 10), + }); + + return true; + }, [authTokenConfig, selectedWallet, couponDuration]); + + const [ + activateCoupon, + isActivatingCoupon, + isCouponActivated, + resetActivateCoupon, + ] = useAsync(activateCouponHandler, onError); + + const removeCoupon = useCallback(() => { + setCoupon(''); + resetApplyCoupon(); + resetActivateCoupon(); + }, []); + + const resetAll = useCallback(() => { + resetSetupPlan(); + resetApplyCoupon(); + resetActivateCoupon(); + setApplyingCouponError(undefined); + setCoupon(''); + setCouponDuration(''); + setSelectedWallet(undefined); + setRetryIndex(v => v + 1); + setUnhandledError(undefined); + walletAuthService.reset(); + encryptMessageService.reset(); + sessionService.reset(); + }, [ + walletAuthService.reset, + encryptMessageService.reset, + resetSetupPlan, + sessionService.reset, + ]); + + const onRetry = useCallback(() => { + const retryLogic = onRetryFuncMap[currentTab]?.[currentDialog]; + + if (retryLogic) { + setRetryIndex(v => v + 1); + retryLogic(); + } else { + resetAll(); + goTo(0, 0); + } + + setUnhandledError(undefined); + }, [currentTab, currentDialog, onRetryFuncMap, walletAuthService.reset]); + + const registerUser = useCallback( + async (params: IUserDetails) => { + const isSuccess = await walletAuthService.registerUser(params); + + if (isSuccess) { + goTo(tabIndicies.email.tabNumber, tabIndicies.email.dialogs.verifyOtp); + } + }, + [walletAuthService.registerUser], + ); + + const onNextActionMapPerDialog = useMemo< + Record boolean) | undefined> | undefined> + >( + () => ({ + [tabIndicies.wallet.tabNumber]: { + [tabIndicies.wallet.dialogs.fetchRequestId]: () => { + if (walletAuthService.currentStep === WalletAuthLoginStep.completed) { + goTo( + tabIndicies.encryption.tabNumber, + tabIndicies.encryption.dialogs.deviceEncryption, + ); + return true; + } + + return false; + }, + [tabIndicies.wallet.dialogs.validateSignature]: () => { + if (!walletAuthService.isRegisterationRequired) { + goTo( + tabIndicies.email.tabNumber, + tabIndicies.email.dialogs.verifyOtp, + ); + return true; + } + + return false; + }, + }, + }), + [walletAuthService.isRegisterationRequired, walletAuthService.currentStep], + ); + + const fallbackToWalletSelect = useCallback(() => { + resetSetupPlan(); + resetApplyCoupon(); + resetActivateCoupon(); + setRetryIndex(v => v + 1); + walletAuthService.reset(); + encryptMessageService.reset(); + sessionService.reset(); + goTo(tabIndicies.wallet.tabNumber, tabIndicies.wallet.dialogs.selectWallet); + }, [ + walletAuthService.reset, + encryptMessageService.reset, + resetSetupPlan, + sessionService.reset, + ]); + + const onNextCallback = useCallback(() => { + const action = onNextActionMapPerDialog[currentTab]?.[currentDialog]; + + let doNext = true; + + if (action) { + doNext = !action(); + } + + if (doNext) { + onNext(); + } + }, [onNext, currentTab, currentDialog]); + + const onPreviousActionMapPerDialog = useMemo< + Record boolean) | undefined> | undefined> + >( + () => ({ + [tabIndicies.email.tabNumber]: { + [tabIndicies.email.dialogs.userDetails]: () => { + fallbackToWalletSelect(); + return true; + }, + [tabIndicies.email.dialogs.verifyOtp]: () => { + fallbackToWalletSelect(); + return true; + }, + }, + }), + [fallbackToWalletSelect], + ); + + const onPreviousCallback = useCallback(() => { + const action = onPreviousActionMapPerDialog[currentTab]?.[currentDialog]; + + let doPrevious = true; + + if (action) { + doPrevious = !action(); + } + + if (doPrevious) { + onPrevious(); + } + }, [onPrevious, currentTab, currentDialog]); + + const onCloseCallback = useCallback(() => { + if (isSetupPlanCompleted || isCouponActivated) { + navigateTo(routes.inheritance.home.path); + } + + onClose(); + }, [isSetupPlanCompleted, isCouponActivated, onClose]); + + const ctx = useMemoReturn({ + onNext: onNextCallback, + onPrevious: onPreviousCallback, + onClose: onCloseCallback, + tabs, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + allWallets, + selectedWallet, + setSelectedWallet, + registerUser, + unhandledError, + onRetry, + retryIndex, + clearErrors, + walletAuthDeviceEvents: walletAuthService.deviceEvents, + walletAuthFetchRequestId, + walletAuthIsFetchingRequestId: walletAuthService.isFetchingRequestId, + walletAuthStart: walletAuthService.startWalletAuth, + walletAuthValidateSignature: walletAuthService.validateSignature, + walletAuthIsValidatingSignature: walletAuthService.isValidatingSignature, + walletAuthStep: walletAuthService.currentStep, + walletAuthAbort: walletAuthService.abortWalletAuth, + onRegister: walletAuthService.registerUser, + isRegisteringUser: walletAuthService.isRegisteringUser, + otpVerificationDetails: walletAuthService.otpVerificationDetails, + verifyOtp: walletAuthService.verifyOtp, + isVerifyingOtp: walletAuthService.isVerifyingOtp, + encryptPinStart, + encryptPinAbort: encryptMessageService.abort, + encryptPinDeviceEvents: encryptMessageService.deviceEvents, + encryptPinIsCompleted: encryptMessageService.isEncrypted, + setupPlan, + isSettingUpPlan, + isSetupPlanCompleted, + isTermsAccepted, + setIsTermsAccepted, + coupon, + applyCoupon, + isApplyingCoupon, + isCouponApplied, + activateCoupon, + isActivatingCoupon, + isCouponActivated, + removeCoupon, + applyingCouponError, + couponDuration, + isEstablishingSession: sessionService.isStartingSession, + isRegisterationRequired: walletAuthService.isRegisterationRequired, + userDetailPrefillData, + }); + + return ( + + {children} + + ); +}; + +export function useInheritanceSilverPlanPurchaseDialog(): InheritanceSilverPlanPurchaseDialogContextInterface { + return useContext(InheritanceSilverPlanPurchaseDialogContext); +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/context/types.ts b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/context/types.ts new file mode 100644 index 000000000..9ac258218 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/context/types.ts @@ -0,0 +1,67 @@ +import { IWallet } from '@cypherock/db-interfaces'; + +import { ITabs } from '~/hooks'; + +import { + IUserDetails, + WalletAuthLoginStep, + IOtpVerificationDetails, +} from '../../hooks'; + +export interface IWalletForSelection extends IWallet { + isDeleted?: boolean; + isActive?: boolean; +} + +export interface InheritanceSilverPlanPurchaseDialogContextInterface { + tabs: ITabs; + onNext: (tab?: number, dialog?: number) => void; + goTo: (tab: number, dialog?: number) => void; + onPrevious: () => void; + currentTab: number; + currentDialog: number; + isDeviceRequired: boolean; + onClose: () => void; + allWallets: IWalletForSelection[]; + selectedWallet?: IWalletForSelection; + setSelectedWallet: (wallet: IWalletForSelection) => void; + registerUser: (params: IUserDetails) => void; + isRegisteringUser: boolean; + unhandledError?: any; + onRetry: () => void; + retryIndex: number; + clearErrors: () => void; + walletAuthDeviceEvents: Record; + walletAuthFetchRequestId: () => void; + walletAuthIsFetchingRequestId: boolean; + walletAuthStart: () => void; + walletAuthAbort: () => void; + walletAuthIsValidatingSignature: boolean; + walletAuthValidateSignature: () => Promise; + walletAuthStep: WalletAuthLoginStep; + otpVerificationDetails?: IOtpVerificationDetails; + verifyOtp: (otp: string) => Promise; + isVerifyingOtp: boolean; + encryptPinStart: () => void; + encryptPinAbort: () => void; + encryptPinDeviceEvents: Record; + encryptPinIsCompleted: boolean; + setupPlan: () => void; + isSettingUpPlan: boolean; + isSetupPlanCompleted: boolean; + isTermsAccepted: boolean; + setIsTermsAccepted: (value: boolean) => void; + coupon: string; + applyCoupon: (coupon: string) => Promise; + isApplyingCoupon: boolean; + isCouponApplied: boolean; + activateCoupon: () => Promise; + isActivatingCoupon: boolean; + isCouponActivated: boolean; + removeCoupon: () => void; + applyingCouponError?: { heading: string; subtext: string }; + couponDuration: string; + isEstablishingSession: boolean; + isRegisterationRequired: boolean; + userDetailPrefillData: IUserDetails; +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/context/useDialogHandler.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/context/useDialogHandler.tsx new file mode 100644 index 000000000..7ce0b491c --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/context/useDialogHandler.tsx @@ -0,0 +1,163 @@ +import React, { useCallback, useMemo } from 'react'; + +import { ITabs, useTabsAndDialogs } from '~/hooks'; +import { + useAppDispatch, + useAppSelector, + selectLanguage, + closeDialog, +} from '~/store'; + +import { + Checkout, + Ensure, + Instructions, + SelectWallet, + FetchRequestId, + WalletAuth, + ValidateSignature, + UserDetails, + VerifyOTP, + DeviceEncryption, + EncryptionLoader, + EncryptionSuccess, + Terms, + Success, +} from '../Dialogs'; + +export const tabIndicies = { + terms: { + tabNumber: 0, + dialogs: { + terms: 0, + }, + }, + instructions: { + tabNumber: 1, + dialogs: { + ensure: 0, + video: 1, + }, + }, + wallet: { + tabNumber: 2, + dialogs: { + selectWallet: 0, + fetchRequestId: 1, + walletAuth: 2, + validateSignature: 3, + }, + }, + email: { + tabNumber: 3, + dialogs: { + userDetails: 0, + verifyOtp: 1, + }, + }, + encryption: { + tabNumber: 4, + dialogs: { + deviceEncryption: 0, + encryptionLoader: 1, + encryptionSuccess: 2, + }, + }, + checkout: { + tabNumber: 5, + dialogs: { + checkout: 0, + }, + }, +}; + +export const useSilverPlanDialogHanlders = () => { + const dispatch = useAppDispatch(); + const lang = useAppSelector(selectLanguage); + + const deviceRequiredDialogsMap: Record = + useMemo( + () => ({ + [tabIndicies.wallet.tabNumber]: [tabIndicies.wallet.dialogs.walletAuth], + [tabIndicies.encryption.tabNumber]: [ + tabIndicies.encryption.dialogs.deviceEncryption, + ], + }), + [], + ); + + const tabs: ITabs = useMemo( + () => [ + { + name: lang.strings.inheritance.termsOfService.title, + dialogs: [], + }, + { + name: lang.strings.inheritanceSilverPlanPurchase.instructions.heading, + dialogs: [, ], + }, + { + name: lang.strings.inheritanceSilverPlanPurchase.wallet.heading, + dialogs: [ + , + , + , + , + ], + }, + { + name: lang.strings.inheritanceSilverPlanPurchase.email.heading, + dialogs: [ + , + , + ], + }, + { + name: lang.strings.inheritanceSilverPlanPurchase.encryption.heading, + dialogs: [ + , + , + , + ], + }, + { + name: lang.strings.inheritanceSilverPlanPurchase.checkout.heading, + dialogs: [], + }, + { + name: '', + dialogs: [], + dontShowOnMilestone: true, + }, + ], + [], + ); + + const { + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + } = useTabsAndDialogs({ + deviceRequiredDialogsMap, + tabs, + dialogName: 'inheritanceSilverPlanPurchase', + }); + + const onClose = useCallback(() => { + dispatch(closeDialog('inheritanceSilverPlanPurchase')); + }, []); + + return { + onClose, + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + tabs, + }; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/index.tsx new file mode 100644 index 000000000..2991b96ca --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SilverPlanPurchase/index.tsx @@ -0,0 +1,108 @@ +import { + BlurOverlay, + CloseButton, + DialogBox, + DialogBoxBackgroundBar, + DialogBoxBody, + MilestoneAside, + WalletDialogMainContainer, +} from '@cypherock/cysync-ui'; +import React, { FC } from 'react'; + +import { + ErrorHandlerDialog, + WithConnectedDevice, + WithConnectedDeviceProps, +} from '~/components'; +import { useAppSelector, selectLanguage } from '~/store'; + +import { + InheritanceSilverPlanPurchaseDialogProvider, + useInheritanceSilverPlanPurchaseDialog, +} from './context'; + +const DeviceConnectionWrapper: React.FC<{ + isDeviceRequired: boolean; + children: React.ReactNode; + withConnectedProps?: WithConnectedDeviceProps; +}> = ({ isDeviceRequired, withConnectedProps, children }) => { + if (isDeviceRequired) + return ( + + {children} + + ); + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children}; +}; + +DeviceConnectionWrapper.defaultProps = { + withConnectedProps: {}, +}; + +const InheritanceSilverPlanPurchase: FC = () => { + const { + tabs, + currentTab, + currentDialog, + unhandledError, + onClose, + onRetry, + isDeviceRequired, + } = useInheritanceSilverPlanPurchaseDialog(); + + const lang = useAppSelector(selectLanguage); + + return ( + + + <> + !t.dontShowOnMilestone) + .map(t => t.name)} + activeTab={currentTab} + heading={lang.strings.inheritanceSilverPlanPurchase.title} + /> + + + + + {tabs[currentTab]?.dialogs[currentDialog]} + + + + onClose()} />} + position="top" + useLightPadding + /> + + + + + ); +}; + +export const InheritanceSilverPlanPurchaseDialog = () => ( + + + +); diff --git a/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Dialogs/EnterEmail.tsx b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Dialogs/EnterEmail.tsx new file mode 100644 index 000000000..3f4a22510 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Dialogs/EnterEmail.tsx @@ -0,0 +1,106 @@ +import { Flex, Input, LangDisplay, Typography } from '@cypherock/cysync-ui'; +import React, { useState } from 'react'; + +import { useStateWithRef } from '~/hooks'; +import { selectLanguage, useAppSelector } from '~/store'; +import { validateEmail } from '~/utils'; + +import { useInheritanceSyncPlansDialog } from '../context'; +import { InheritanceSyncPlansLayout } from '../Layout'; + +export const EnterEmail: React.FC = () => { + const lang = useAppSelector(selectLanguage); + const { + onClose, + onEnterEmail, + email: existingEmail, + isSendingEmail, + } = useInheritanceSyncPlansDialog(); + const strings = lang.strings.dialogs.inheritanceSyncPlans; + + const [email, setEmail] = useStateWithRef(existingEmail); + const [validationError, setValidationError] = useState( + undefined, + ); + + const validateInputEmail = (newEmail: string) => { + if (newEmail.length === 0) { + setValidationError(undefined); + return; + } + + const validation = validateEmail(newEmail, lang); + + if (!validation.success) { + setValidationError(validation.error.issues[0].message); + return; + } + + setValidationError(undefined); + }; + + const onEmailChange = (val: string) => { + setEmail(val); + validateInputEmail(val); + }; + + const onSubmit = (e?: React.FormEvent) => { + if (e) { + e.preventDefault(); + } + + if (!validationError) { + onEnterEmail(email); + } + }; + + const formId = 'inheritance-sync-plans-enter-email'; + const displayError = validationError; + + return ( + +
+ + + {strings.enterEmail.title} + + + {strings.enterEmail.subTitle} + + + {displayError && ( + + + + )} + +
+
+ ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Dialogs/VerifyEmail.tsx b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Dialogs/VerifyEmail.tsx new file mode 100644 index 000000000..dfeab732a --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Dialogs/VerifyEmail.tsx @@ -0,0 +1,68 @@ +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; + +import { OTPInputDialog, OTPInputDialogRef } from '../../components'; +import { useInheritanceSyncPlansDialog } from '../context'; + +export const VerifyEmail: React.FC = () => { + const { + onClose, + email, + onPrevious, + verifyEmail, + isVerifyingEmail, + otpVerificationDetails, + } = useInheritanceSyncPlansDialog(); + + const otpRef = useRef(null); + + const onVerify = useCallback( + (otp: string) => { + verifyEmail(otp); + }, + [verifyEmail], + ); + + const onResendOtp = () => { + // TODO: resend otp + }; + + const otpExpireTime = useMemo( + () => otpVerificationDetails?.otpExpiry ?? '', + [otpVerificationDetails?.otpExpiry], + ); + + const otpLength = 6; + + const retriesRemaining = otpVerificationDetails?.retriesRemaining ?? 3; + + useEffect(() => { + if (otpRef.current) { + otpRef.current.reset(); + } + }, [otpVerificationDetails?.id]); + + useEffect(() => { + if (!otpVerificationDetails) onClose(); + }, [otpVerificationDetails]); + + if (!otpVerificationDetails) { + return null; + } + + return ( + + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Dialogs/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Dialogs/index.tsx new file mode 100644 index 000000000..cba24ca18 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Dialogs/index.tsx @@ -0,0 +1,2 @@ +export * from './EnterEmail'; +export * from './VerifyEmail'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Layout.tsx b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Layout.tsx new file mode 100644 index 000000000..744aa52d8 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/Layout.tsx @@ -0,0 +1,73 @@ +import { + DialogBox, + DialogBoxHeader, + LangDisplay, + CloseButton, + ScrollableContainer, + DialogBoxBody, + DialogBoxFooter, + Button, + ButtonProps, +} from '@cypherock/cysync-ui'; +import React from 'react'; + +export interface IInheritanceSyncPlansLayoutProps { + children: React.ReactNode; + onClose: () => void; + actionButtonText?: string; + onActionButtonClick?: () => void; + isActionButtonDisabled?: boolean; + isActionButtonLoading?: boolean; + actionButtonVariant?: ButtonProps['variant']; + formId?: string; +} + +export const InheritanceSyncPlansLayout: React.FC< + IInheritanceSyncPlansLayoutProps +> = ({ + children, + onClose, + actionButtonText, + onActionButtonClick, + isActionButtonDisabled, + isActionButtonLoading, + actionButtonVariant, + formId, +}) => ( + + + + + + + {children} + + + {actionButtonText && onActionButtonClick && ( + + + + )} + +); + +InheritanceSyncPlansLayout.defaultProps = { + actionButtonText: undefined, + onActionButtonClick: undefined, + isActionButtonDisabled: undefined, + actionButtonVariant: undefined, + isActionButtonLoading: undefined, + formId: undefined, +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/context/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/context/index.tsx new file mode 100644 index 000000000..d18921e8d --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/context/index.tsx @@ -0,0 +1,286 @@ +import { ServerErrorType } from '@cypherock/cysync-core-constants'; +import { insertInheritancePlan } from '@cypherock/cysync-core-services'; +import { + IInheritancePlan, + InheritancePlanType, + InheritancePlanTypeMap, +} from '@cypherock/db-interfaces'; +import React, { + Context, + FC, + ReactNode, + createContext, + useCallback, + useContext, + useState, +} from 'react'; + +import { + ITabs, + useAsync, + useMemoReturn, + useStateWithRef, + useTabsAndDialogs, +} from '~/hooks'; +import { + inheritancePlanService, + inheritanceSyncPlansService, + InheritanceSyncPlansInitResponse, + InheritanceUserTypeMap, +} from '~/services'; +import { closeDialog, useAppDispatch } from '~/store'; +import { getDB } from '~/utils'; +import logger from '~/utils/logger'; + +import { EnterEmail, VerifyEmail } from '../Dialogs'; + +export interface IOtpVerificationDetails { + id: string; + email: string; + retriesRemaining: number; + otpExpiry: string; + showIncorrectError?: boolean; +} + +export interface InheritanceSyncPlansDialogContextInterface { + tabs: ITabs; + onNext: (tab?: number, dialog?: number) => void; + goTo: (tab: number, dialog?: number) => void; + onPrevious: () => void; + currentTab: number; + currentDialog: number; + isDeviceRequired: boolean; + onClose: () => void; + email: string; + onEnterEmail: (email: string) => void; + isSendingEmail: boolean; + startResponse?: InheritanceSyncPlansInitResponse; + unhandledError?: any; + onRetry: () => void; + verifyEmail: (otp: string) => void; + isVerifyingEmail: boolean; + otpVerificationDetails?: IOtpVerificationDetails; +} + +export const InheritanceSyncPlansDialogContext: Context = + createContext( + {} as InheritanceSyncPlansDialogContextInterface, + ); + +export interface InheritanceSyncPlansDialogContextProviderProps { + children: ReactNode; +} + +export const InheritanceSyncPlansDialogProvider: FC< + InheritanceSyncPlansDialogContextProviderProps +> = ({ children }) => { + const dispatch = useAppDispatch(); + + const deviceRequiredDialogsMap: Record = {}; + const tabs: ITabs = [ + { + name: 'Enter Email', + dialogs: [], + }, + { + name: 'Verify Email', + dialogs: [], + }, + ]; + + const { + onNext, + onPrevious, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + } = useTabsAndDialogs({ + deviceRequiredDialogsMap, + tabs, + dialogName: 'inheritanceSyncPlans', + }); + + const onClose = () => { + dispatch(closeDialog('inheritanceSyncPlans')); + }; + + const [email, setEmail] = useState(''); + const [startResponse, setStartResponse] = useState< + InheritanceSyncPlansInitResponse | undefined + >(undefined); + const [unhandledError, setUnhandledError] = useState(); + const [ + otpVerificationDetails, + setOtpVerificationDetails, + otpVerificationDetailsRef, + ] = useStateWithRef(undefined); + + const onError = useCallback((e?: any) => { + setUnhandledError(e); + }, []); + + const sendEmailHanlder = useCallback(async (emailId: string) => { + setEmail(emailId); + const response = await inheritancePlanService.sync.init({ email: emailId }); + + if (response.error) { + throw response.error; + } + + const otpDetails = response.result.otpDetails?.[0]; + + if (otpDetails) { + setOtpVerificationDetails({ + id: 'emailVerification', + email: emailId, + ...otpDetails, + }); + } + + setStartResponse(response.result); + goTo(1); + + return true; + }, []); + + const [sendEmail, isSendingEmail, , resetSendEmail] = useAsync( + sendEmailHanlder, + onError, + ); + + const onRetry = useCallback(() => { + resetSendEmail(); + setUnhandledError(undefined); + setStartResponse(undefined); + goTo(0); + }, []); + + const verifyEmailHandler = useCallback( + async (otp: string) => { + if (!startResponse || !otpVerificationDetailsRef.current) { + return false; + } + + const response = await inheritanceSyncPlansService.verify({ + requestId: startResponse.requestId, + otp, + }); + + if (response.error) { + if (response.error.code === ServerErrorType.OTP_VERIFICATION_FAILED) { + setOtpVerificationDetails({ + ...otpVerificationDetailsRef.current, + showIncorrectError: true, + otpExpiry: + response.error.details?.responseBody.otpExpiry ?? + otpVerificationDetailsRef.current.otpExpiry, + retriesRemaining: + response.error.details?.responseBody.retriesRemaining ?? + otpVerificationDetailsRef.current.retriesRemaining, + }); + return false; + } + + throw response.error; + } + + const db = getDB(); + + if (response.result.wallets) { + for (const wallet of response.result.wallets) { + const isNominee = wallet.role === InheritanceUserTypeMap.nominee; + const isOwner = wallet.role === InheritanceUserTypeMap.owner; + + if (!isNominee && !isOwner) { + logger.warn( + `SyncPlans: Wallet ${wallet._id} is not a nominee or owner`, + ); + continue; + } + + if (isNominee && isOwner) { + logger.warn( + `SyncPlans: Wallet ${wallet._id} is both a nominee and owner`, + ); + continue; + } + + const order = wallet.subscription?.[0]?.order; + let serverPlanType = order?.planType; + if (!serverPlanType) { + logger.warn("SyncPlans: Wallet doesn't have plan type"); + continue; + } + serverPlanType = serverPlanType.toLowerCase(); + + if ( + !(Object.values(InheritancePlanTypeMap) as any).includes( + serverPlanType, + ) + ) { + logger.warn(`SyncPlans: Unknown plan type ${serverPlanType}`); + continue; + } + + const plan: IInheritancePlan = { + walletId: wallet.wallet, + walletName: wallet.fullName ?? '', + isNominee, + purchasedAt: order?.activationDate + ? new Date(order.activationDate).getTime() + : undefined, + expireAt: order?.expiryDate + ? new Date(order.expiryDate).getTime() + : undefined, + type: serverPlanType as InheritancePlanType, + }; + + await insertInheritancePlan(db, plan); + } + } + + setOtpVerificationDetails(undefined); + + return true; + }, + [startResponse], + ); + + const [verifyEmail, isVerifyingEmail] = useAsync(verifyEmailHandler, onError); + + const onResendOtp = useCallback(async () => { + // TODO: Implement this function + }, []); + + const ctx = useMemoReturn({ + onNext, + onPrevious, + tabs, + onClose, + goTo, + currentTab, + currentDialog, + isDeviceRequired, + email, + onEnterEmail: sendEmail, + isSendingEmail, + startResponse, + unhandledError, + onRetry, + verifyEmail, + isVerifyingEmail, + onResendOtp, + otpVerificationDetails, + }); + + return ( + + {children} + + ); +}; + +export function useInheritanceSyncPlansDialog(): InheritanceSyncPlansDialogContextInterface { + return useContext(InheritanceSyncPlansDialogContext); +} diff --git a/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/index.tsx b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/index.tsx new file mode 100644 index 000000000..0b90f1b79 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/SyncPlans/index.tsx @@ -0,0 +1,34 @@ +import { BlurOverlay } from '@cypherock/cysync-ui'; +import React, { FC } from 'react'; + +import { ErrorHandlerDialog } from '~/components'; + +import { + InheritanceSyncPlansDialogProvider, + useInheritanceSyncPlansDialog, +} from './context'; + +const InheritanceSyncPlans: FC = () => { + const { tabs, currentTab, currentDialog, unhandledError, onClose, onRetry } = + useInheritanceSyncPlansDialog(); + + return ( + + + {tabs[currentTab]?.dialogs[currentDialog]} + + + ); +}; + +export const InheritanceSyncPlansDialog = () => ( + + + +); diff --git a/packages/cysync-core/src/dialogs/Inheritance/components/OTPInputDialog.tsx b/packages/cysync-core/src/dialogs/Inheritance/components/OTPInputDialog.tsx new file mode 100644 index 000000000..e8fe2a26f --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/components/OTPInputDialog.tsx @@ -0,0 +1,223 @@ +import { + Button, + CloseButton, + DialogBox, + DialogBoxBody, + DialogBoxFooter, + DialogBoxHeader, + Flex, + LangDisplay, + OTPInput, + OTPInputStatus, + ScrollableContainer, + Typography, +} from '@cypherock/cysync-ui'; +import { formatSecondsToMinutes } from '@cypherock/cysync-utils'; +import React, { + ForwardedRef, + forwardRef, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from 'react'; + +import { useCountdown } from '~/hooks'; +import { selectLanguage, useAppSelector } from '~/store'; + +export interface OTPInputDialogRef { + reset: () => void; +} + +export interface OTPInputDialogProps { + showHeader?: boolean; + onClose?: () => void; + emails: string | string[]; + onBack: () => void; + otpLength: number; + onVerify: (otp: string) => void; + isVerifyingEmail: boolean; + isResendingOtp: boolean; + onResendOtp: () => void; + retriesRemaining: number; + otpExpireTime?: string; + wrongOtpError?: boolean; + title?: string; +} + +export const OTPInputDialog: React.FC< + OTPInputDialogProps & { ref?: ForwardedRef } +> = forwardRef( + ( + { + onClose, + emails, + onBack, + otpLength, + onVerify, + isVerifyingEmail, + isResendingOtp, + onResendOtp, + retriesRemaining, + otpExpireTime, + wrongOtpError, + title, + showHeader = false, + }, + ref, + ) => { + const lang = useAppSelector(selectLanguage); + + const strings = lang.strings.dialogs.inheritanceSyncPlans; + + const [otp, setOtp] = useState(''); + const { seconds: expireSeconds } = useCountdown( + otpExpireTime ? new Date(otpExpireTime).getTime() : new Date().getTime(), + ); + + const onOtpChange = (val: string) => { + setOtp(val); + + if (val.length === otpLength) { + onVerify(val); + } + }; + + const onResend = () => { + onResendOtp(); + }; + + const textVariables = useMemo( + () => ({ + email: (Array.isArray(emails) ? emails.join(' & ') : emails).replace( + /\*/g, + `\\*`, + ), + retries: retriesRemaining.toString(), + timeout: formatSecondsToMinutes(expireSeconds), + }), + [emails, retriesRemaining, expireSeconds], + ); + + const getStatus = (): OTPInputStatus => { + if (retriesRemaining <= 0) { + return 'retryExceeded'; + } + + if (wrongOtpError) { + return 'error'; + } + + return 'idle'; + }; + + const getTitle = () => { + if (retriesRemaining <= 0) { + return lang.strings.otp.noRetries.title; + } + + if (wrongOtpError) { + return lang.strings.otp.wrongOtpTitle; + } + + return lang.strings.otp.title; + }; + + const getActionText = () => { + if (expireSeconds > 0) { + return lang.strings.buttons.resendOTP; + } + return lang.strings.otp.buttons.resendWithTimeout; + }; + + useEffect(() => { + if (!isVerifyingEmail) { + setOtp(''); + } + }, [isVerifyingEmail]); + + useImperativeHandle( + ref, + () => ({ + reset: () => { + setOtp(''); + }, + }), + [], + ); + + const otpTitle = getTitle(); + const status = getStatus(); + const actionText = getActionText(); + + return ( + + {onClose && showHeader && ( + + + + )} + + + + + {title ?? strings.verifyEmail.title} + + + + + + + + + + ); + }, +); + +OTPInputDialog.displayName = 'OTPInputDialog'; + +OTPInputDialog.defaultProps = { + wrongOtpError: undefined, + otpExpireTime: undefined, + title: undefined, + onClose: undefined, + showHeader: false, +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/components/UserDetailsForm.tsx b/packages/cysync-core/src/dialogs/Inheritance/components/UserDetailsForm.tsx new file mode 100644 index 000000000..4122ab7e0 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/components/UserDetailsForm.tsx @@ -0,0 +1,185 @@ +import { constants } from '@cypherock/cysync-core-constants'; +import { + Container, + Typography, + LangDisplay, + Input, +} from '@cypherock/cysync-ui'; +import React, { FC, useEffect, useMemo, useState } from 'react'; +import { z } from 'zod'; + +import { selectLanguage, useAppSelector } from '~/store'; +import { getEmailValidationSchema } from '~/utils'; + +interface UserDetailsFormProps { + onSubmit: (e: React.FormEvent) => void; + formId: string; + strings: { + title: string; + subTitle?: string; + form: { + name: string; + emailField: { + label: string; + tooltip?: string; + }; + alternateEmail: string; + }; + }; + name: string; + setName: (name: string) => void; + isSubmittingUserDetails: boolean; + email: string; + setEmail: (email: string) => void; + alternateEmail: string; + setAlternateEmail: (altEmail: string) => void; + isAlternateEmailRequired?: boolean; + isSameEmail?: boolean; + setHasErrors?: (val: boolean) => void; +} + +export const UserDetailsForm: FC = ({ + onSubmit, + formId, + strings, + name, + setName, + isSubmittingUserDetails, + email, + setEmail, + alternateEmail, + setAlternateEmail, + isAlternateEmailRequired, + isSameEmail, + setHasErrors, +}) => { + const lang = useAppSelector(selectLanguage); + const userDetailsStrings = lang.strings.inheritance.dialog.userDetails; + const [userDetailsError, setUserDetailsError] = useState(''); + + const schema = z.object({ + name: z + .string() + .min(1, { + message: lang.strings.validation.generic.required, + }) + .max(50), + email: getEmailValidationSchema(lang), + alternateEmail: getEmailValidationSchema(lang, isAlternateEmailRequired), + }); + + const [errorKey, setErrorKey] = useState(''); + + const errorText = useMemo(() => { + if (userDetailsError) return userDetailsError; + if (isSameEmail) return userDetailsStrings.error.sameEmail; + return ''; + }, [isSameEmail, userDetailsError]); + + useEffect(() => { + setHasErrors?.(false); + if (!email && !alternateEmail) return; + const validation = schema.safeParse({ email, alternateEmail, name }); + setErrorKey(''); + setUserDetailsError(''); + + if (!validation.success) { + const key = Object.keys(validation.error.formErrors.fieldErrors)[0]; + const error = (validation.error.formErrors.fieldErrors as any)[key][0]; + setErrorKey(key); + setUserDetailsError(error); + setHasErrors?.(true); + } + }, [email, alternateEmail, name]); + + return ( +
+ + + + + + + + + + + + + {errorText && ( + + {errorText} + + )} +
+ ); +}; + +UserDetailsForm.defaultProps = { + isAlternateEmailRequired: undefined, + isSameEmail: undefined, + setHasErrors: undefined, +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/components/index.ts b/packages/cysync-core/src/dialogs/Inheritance/components/index.ts new file mode 100644 index 000000000..b6eafc5e2 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/components/index.ts @@ -0,0 +1,2 @@ +export * from './OTPInputDialog'; +export * from './UserDetailsForm'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/hooks/index.ts b/packages/cysync-core/src/dialogs/Inheritance/hooks/index.ts new file mode 100644 index 000000000..b5b4a8899 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/hooks/index.ts @@ -0,0 +1,4 @@ +export * from './useWalletAuth'; +export * from './useEncryptMessage'; +export * from './useDecryptMessage'; +export * from './useSession'; diff --git a/packages/cysync-core/src/dialogs/Inheritance/hooks/useAuthConfig.tsx b/packages/cysync-core/src/dialogs/Inheritance/hooks/useAuthConfig.tsx new file mode 100644 index 000000000..918e031a8 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/hooks/useAuthConfig.tsx @@ -0,0 +1,63 @@ +import { assert } from '@cypherock/cysync-utils'; +import { useMemo } from 'react'; +import { + IWalletAuthTokens, + selectInheritanceSeedAuthTokens, + selectInheritanceWalletAuthTokens, + updateSeedAuthTokens, + updateWalletAuthTokens, + useAppSelector, +} from '~/store'; + +export type AuthTokenType = 'SEED' | 'WALLET'; + +const updateAuthTokens = { + SEED: updateSeedAuthTokens, + WALLET: updateWalletAuthTokens, +} as const; + +export const useAuthTokenConfig = (props: { + walletId: string; + authType: AuthTokenType; +}) => { + const walletAuthTokens = useAppSelector(selectInheritanceWalletAuthTokens); + const seedAuthTokens = useAppSelector(selectInheritanceSeedAuthTokens); + + const authTokens = useMemo(() => { + if (props.authType === 'SEED') return seedAuthTokens; + if (props.authType === 'WALLET') return walletAuthTokens; + return undefined; + }, [props.authType]); + + const authTokenConfig = useMemo(() => { + const accessToken = authTokens?.[props.walletId]?.accessToken; + const refreshToken = authTokens?.[props.walletId]?.refreshToken; + + assert(accessToken, 'accessToken not found'); + assert(refreshToken, 'refreshToken not found'); + + const updateAuthToken = (token: string) => { + const authToken: IWalletAuthTokens = { + accessToken: token, + refreshToken, + }; + + updateAuthTokens[props.authType]({ + walletId: props.walletId, + authTokens: authToken, + }); + }; + + return { + accessToken, + refreshTokenConfig: { + refreshToken, + updateAuthToken, + }, + }; + }, [props]); + + return { + authTokenConfig, + }; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/hooks/useCatpureUnhandledErrors.tsx b/packages/cysync-core/src/dialogs/Inheritance/hooks/useCatpureUnhandledErrors.tsx new file mode 100644 index 000000000..c4101d598 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/hooks/useCatpureUnhandledErrors.tsx @@ -0,0 +1,24 @@ +import { useState } from 'react'; + +export const useCaptureUnhandledErrors = () => { + const [unhandledError, setUnhandledError] = useState(); + + function captureErrors Promise>(fn: T): T { + return async function ( + ...args: Parameters + ): Promise | void> { + try { + return await fn(...args); + } catch (error) { + setUnhandledError(error); + } + return undefined; + } as T; + } + + return { + unhandledError, + setUnhandledError, + captureErrors, + }; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/hooks/useDecryptMessage.tsx b/packages/cysync-core/src/dialogs/Inheritance/hooks/useDecryptMessage.tsx new file mode 100644 index 000000000..216b05f52 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/hooks/useDecryptMessage.tsx @@ -0,0 +1,127 @@ +import { + ENCRYPTED_DATA_SERIALIZATION_TAGS, + IInheritanceDecryptMessageEvent, +} from '@cypherock/app-support-inheritance'; +import lodash from 'lodash'; +import { useState, useRef, useCallback } from 'react'; +import { Subscription, Observer } from 'rxjs'; + +import { deviceLock, useDevice } from '~/context'; +import { inheritanceSupport } from '~/utils'; +import logger from '~/utils/logger'; + +export const useDecryptMessage = (onErrorCallback: (e?: any) => void) => { + const { connection } = useDevice(); + + const [deviceEvents, setDeviceEvents] = useState< + Record + >({}); + const flowSubscription = useRef(); + const walletIdRef = useRef(); + const [personalMessage, setPersonalMessage] = useState(); + const [cardLocation, setCardLocation] = useState(); + const [isDecrypted, setIsDecrypted] = useState(false); + + const cleanUp = useCallback(() => { + if (flowSubscription.current) { + flowSubscription.current.unsubscribe(); + flowSubscription.current = undefined; + } + }, []); + + const onError = useCallback( + (e?: any) => { + console.log(e); + logger.error('Error on inheritance decrypt message flow'); + logger.error(e); + cleanUp(); + onErrorCallback(e); + }, + [onErrorCallback, cleanUp], + ); + + const getFlowObserver = useCallback( + (onEnd: () => void): Observer => ({ + next: payload => { + if (payload.device) setDeviceEvents({ ...payload.device.events }); + if (payload.decryptedMessages) { + setPersonalMessage( + payload.decryptedMessages[ + ENCRYPTED_DATA_SERIALIZATION_TAGS.NOMINEE_MESSAGE + ], + ); + setCardLocation( + payload.decryptedMessages[ + ENCRYPTED_DATA_SERIALIZATION_TAGS.WALLET_MESSAGE + ], + ); + setIsDecrypted(true); + } + }, + error: err => { + onEnd(); + onError(err); + }, + complete: () => { + cleanUp(); + onEnd(); + }, + }), + [onError], + ); + + const start = useCallback( + async (walletId: string, message: string) => { + logger.info('Starting inheritance decrypt message'); + + if (!connection?.connection) { + return; + } + + try { + cleanUp(); + walletIdRef.current = walletId; + + const taskId = lodash.uniqueId('task-'); + + await deviceLock.acquire(connection.device, taskId); + + const onEnd = () => { + deviceLock.release(connection.device, taskId); + }; + + const deviceConnection = connection.connection; + + flowSubscription.current = inheritanceSupport + .decryptMessageWithPin({ + connection: deviceConnection, + walletId: walletIdRef.current, + message, + }) + .subscribe(getFlowObserver(onEnd)); + } catch (e) { + onError(e); + } + }, + [connection], + ); + + const reset = useCallback(() => { + walletIdRef.current = undefined; + setIsDecrypted(false); + setPersonalMessage(undefined); + setCardLocation(undefined); + setDeviceEvents({}); + cleanUp(); + }, [cleanUp]); + + return { + deviceEvents, + reset, + abort: cleanUp, + start, + personalMessage, + cardLocation, + isDecrypted, + }; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/hooks/useEncryptMessage.tsx b/packages/cysync-core/src/dialogs/Inheritance/hooks/useEncryptMessage.tsx new file mode 100644 index 000000000..70ff0d1d3 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/hooks/useEncryptMessage.tsx @@ -0,0 +1,122 @@ +import { IInheritanceEncryptMessageEvent } from '@cypherock/app-support-inheritance'; +import lodash from 'lodash'; +import { useState, useRef, useCallback } from 'react'; +import { Subscription, Observer } from 'rxjs'; + +import { deviceLock, useDevice } from '~/context'; +import { inheritanceSupport } from '~/utils'; +import logger from '~/utils/logger'; + +export const useEncryptMessage = (onErrorCallback: (e?: any) => void) => { + const { connection } = useDevice(); + + const [deviceEvents, setDeviceEvents] = useState< + Record + >({}); + const flowSubscription = useRef(); + const walletIdRef = useRef(); + const [encryptedMessages, setEncryptedMessages] = useState< + string | undefined + >(); + const [isEncrypted, setIsEncrypted] = useState(false); + + const cleanUp = useCallback(() => { + if (flowSubscription.current) { + flowSubscription.current.unsubscribe(); + flowSubscription.current = undefined; + } + }, []); + + const onError = useCallback( + (e?: any) => { + console.log(e); + logger.error('Error on inheritance encrypt message flow'); + logger.error(e); + cleanUp(); + onErrorCallback(e); + }, + [onErrorCallback, cleanUp], + ); + + const getFlowObserver = useCallback( + (onEnd: () => void): Observer => ({ + next: payload => { + if (payload.device) setDeviceEvents({ ...payload.device.events }); + if (payload.encryptedMessages) { + setEncryptedMessages(payload.encryptedMessages); + setIsEncrypted(true); + } + }, + error: err => { + onEnd(); + onError(err); + }, + complete: () => { + cleanUp(); + onEnd(); + }, + }), + [onError], + ); + + const start = useCallback( + async ( + walletId: string, + messages?: { personalMessage?: string; cardLocation?: string }, + ) => { + logger.info('Starting inheritance encrypt message'); + + if (!connection?.connection) { + return; + } + + let onEnd: (() => void) | undefined; + try { + cleanUp(); + walletIdRef.current = walletId; + + const taskId = lodash.uniqueId('task-'); + + await deviceLock.acquire(connection.device, taskId); + + onEnd = () => { + deviceLock.release(connection.device, taskId); + }; + + const deviceConnection = connection.connection; + + flowSubscription.current = inheritanceSupport + .encryptMessageWithPin({ + connection: deviceConnection, + walletId: walletIdRef.current, + personalMessage: messages?.personalMessage, + cardLocation: messages?.cardLocation, + }) + .subscribe(getFlowObserver(onEnd)); + } catch (e) { + if (onEnd) { + onEnd(); + } + onError(e); + } + }, + [connection], + ); + + const reset = useCallback(() => { + walletIdRef.current = undefined; + setIsEncrypted(false); + setEncryptedMessages(undefined); + setDeviceEvents({}); + cleanUp(); + }, [cleanUp]); + + return { + deviceEvents, + reset, + abort: cleanUp, + start, + encryptedMessages, + isEncrypted, + }; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/hooks/useSession.tsx b/packages/cysync-core/src/dialogs/Inheritance/hooks/useSession.tsx new file mode 100644 index 000000000..b32f43b3f --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/hooks/useSession.tsx @@ -0,0 +1,146 @@ +import lodash from 'lodash'; +import { useState, useCallback, useEffect } from 'react'; +import { lastValueFrom } from 'rxjs'; + +import { deviceLock, useDevice } from '~/context'; +import { useAsync } from '~/hooks'; +import { inheritanceSupport } from '~/utils'; +import logger from '~/utils/logger'; + +export const useSession = (onErrorCallback: (e?: any) => void) => { + const { connection } = useDevice(); + + const [sessionId, setSessionId] = useState(); + const [sessionAge, setSessionAge] = useState(); + const [isSessionStarted, setIsSessionStarted] = useState(false); + const [isStartingSession, setIsStartingSession] = useState(false); + + const onError = useCallback( + (e?: any) => { + console.log(e); + logger.error('Error on inheritance session flow'); + logger.error(e); + onErrorCallback(e); + }, + [onErrorCallback], + ); + + const start = useCallback(async (): Promise => { + logger.info('Starting inheritance startSession'); + + if (!connection?.connection) { + return undefined; + } + + setIsStartingSession(true); + let onEnd: (() => void) | undefined; + try { + const taskId = lodash.uniqueId('task-'); + + await deviceLock.acquire(connection.device, taskId); + + onEnd = () => { + deviceLock.release(connection.device, taskId); + }; + + const deviceConnection = connection.connection; + + const payload = await lastValueFrom( + inheritanceSupport.startSession({ + connection: deviceConnection, + }), + ); + + if (payload.sessionId && payload.sessionAge) { + setIsSessionStarted(true); + setSessionId(payload.sessionId); + setSessionAge(payload.sessionAge); + } + + setIsStartingSession(false); + return payload.sessionId; + } catch (e) { + if (onEnd) { + onEnd(); + } + onError(e); + setIsStartingSession(false); + return undefined; + } + }, [connection]); + + const stopHandler = useCallback(async () => { + logger.info('Starting inheritance stopSession'); + + if (!connection?.connection) { + return false; + } + + let onEnd: (() => void) | undefined; + try { + const taskId = lodash.uniqueId('task-'); + + await deviceLock.acquire(connection.device, taskId); + + onEnd = () => { + deviceLock.release(connection.device, taskId); + }; + + const deviceConnection = connection.connection; + + await lastValueFrom( + inheritanceSupport.stopSession({ + connection: deviceConnection, + }), + ); + + setSessionId(undefined); + setSessionAge(undefined); + setIsSessionStarted(false); + + return true; + } catch (e) { + if (onEnd) { + onEnd(); + } + onError(e); + return false; + } + }, [connection]); + + const [stop, isStoppingSession] = useAsync(stopHandler, onError); + + const reset = useCallback(() => { + setIsSessionStarted(false); + setSessionId(undefined); + setSessionAge(undefined); + }, []); + + useEffect(() => { + reset(); + }, [connection]); + + const getIsActive = useCallback(async () => { + if (!isSessionStarted) { + return undefined; + } + + if (sessionId && sessionAge && Date.now() < sessionAge) { + return sessionId; + } + + return undefined; + }, [sessionId, sessionAge, isSessionStarted]); + + return { + reset, + start, + sessionId, + sessionAge, + getIsActive, + isSessionStarted, + stop, + isStartingSession, + isStoppingSession, + }; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/hooks/useUpdatePlanDetails.tsx b/packages/cysync-core/src/dialogs/Inheritance/hooks/useUpdatePlanDetails.tsx new file mode 100644 index 000000000..9962e9794 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/hooks/useUpdatePlanDetails.tsx @@ -0,0 +1,75 @@ +import { assert } from '@cypherock/cysync-utils'; +import { useCallback } from 'react'; +import { inheritancePlanService } from '~/services'; +import { AuthTokenConfig } from '~/services/utils'; +import { + IInheritancePlanDetails, + reminderPeriodStringToNumMap, + updateInheritancePlanDetails, + useAppDispatch, +} from '~/store'; + +export const useUpdatePlanDetails = (props: { + authTokenConfig?: AuthTokenConfig; +}) => { + const dispatch = useAppDispatch(); + + const fetchAndUpdatePlan = useCallback(async () => { + assert(props.authTokenConfig, 'No authTokenConfig provided'); + const result = await inheritancePlanService.getPlan({ + authTokenConfig: props.authTokenConfig, + }); + + if (result.error) { + throw result.error; + } + + const activationDate = + result.result.subscription?.[0]?.activationDate ?? + new Date().toISOString(); + + const expiryDate = + result.result.subscription?.[0]?.order?.expiryDate ?? + Date.now() + 2 * 365 * 24 * 60 * 60 * 1000; + + const planDetails: IInheritancePlanDetails = { + walletId: result.result.wallet, + name: result.result.fullName ?? '', + nominee: + result.result.nominee?.map(n => ({ + name: n.name ?? '', + email: n.email ?? '', + alternateEmail: n.alternateEmail ?? '', + })) ?? [], + ...(result.result.executor !== undefined + ? { + executor: { + name: result.result.executor?.name ?? '', + email: result.result.executor?.email ?? '', + alternateEmail: result.result.executor?.alternateEmail ?? '', + }, + } + : {}), + owner: { + name: result.result.owner?.name ?? '', + email: result.result.owner?.email ?? '', + alternateEmail: result.result.owner?.alternateEmail ?? '', + }, + activationDate: new Date(activationDate).getTime(), + expiryDate: new Date(expiryDate).getTime(), + reminderPeriod: + reminderPeriodStringToNumMap[ + result.result.emailConfig?.frequency ?? '' + ], + }; + + dispatch( + updateInheritancePlanDetails({ + walletId: planDetails.walletId, + planDetails, + }), + ); + }, [props, dispatch]); + + return { fetchAndUpdatePlan }; +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/hooks/useWalletAuth/index.ts b/packages/cysync-core/src/dialogs/Inheritance/hooks/useWalletAuth/index.ts new file mode 100644 index 000000000..a51b45d01 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/hooks/useWalletAuth/index.ts @@ -0,0 +1,446 @@ +import { ServerErrorType } from '@cypherock/cysync-core-constants'; +import { useState, useRef, useCallback, useMemo } from 'react'; + +import { useAsync, useMemoReturn, useStateWithRef } from '~/hooks'; +import { + InheritanceLoginAuthTypeMap, + InheritanceLoginConcernMap, + InheritanceLoginEmailTypeMap, + InheritanceLoginInitResponse, + InheritanceLoginRegisterResponse, + inheritanceLoginService, + InheritanceUserType, + InheritanceUserTypeMap, + InheritanceLoginVerifyResponse, +} from '~/services'; +import { AuthTokenConfig, ServerResponseWithError } from '~/services/utils'; +import { + IWalletAuthTokens, + selectInheritanceSeedAuthTokens, + selectInheritanceWalletAuthTokens, + updateSeedAuthTokens, + updateWalletAuthTokens, + useAppDispatch, + useAppSelector, +} from '~/store'; +import logger from '~/utils/logger'; + +import { + WalletAuthLoginStep, + IUserDetails, + IOtpVerificationDetails, + OtpVerificationConcern, +} from './types'; +import { useWalletAuthDevice } from './useWalletAuthDevice'; + +export * from './types'; + +type AuthType = 'seed-based' | 'wallet-based'; + +export const useWalletAuth = ( + onErrorCallback: (e?: any) => void, + getPrefillData?: (data: IUserDetails) => void, +) => { + const dispatch = useAppDispatch(); + + const walletAuthTokensPerWallet = useAppSelector( + selectInheritanceWalletAuthTokens, + ); + const seedAuthTokensPerWallet = useAppSelector( + selectInheritanceSeedAuthTokens, + ); + + const [currentStep, setCurrentStep] = useState( + WalletAuthLoginStep.fetchRequestId, + ); + + const walletIdRef = useRef(); + const loginTypeRef = useRef(); + const authTypeRef = useRef(); + const initResponse = useRef(); + const registerResponse = useRef< + InheritanceLoginRegisterResponse | undefined + >(); + const userDetails = useRef(); + const [ + otpVerificationDetails, + setOtpVerificationDetails, + otpVerificationDetailsRef, + ] = useStateWithRef(undefined); + const [isRegisterationRequired, setIsRegisterationRequired] = useState(false); + const [authTokens, setAuthTokens] = useState( + undefined, + ); + + const authTokenConfig = useMemo(() => { + if (!authTokens) return undefined; + + const updateAuthToken = (newAccessToken: string) => { + setAuthTokens({ + accessToken: newAccessToken, + refreshToken: authTokens.refreshToken, + }); + }; + return { + accessToken: authTokens.accessToken, + refreshTokenConfig: { + refreshToken: authTokens.refreshToken, + updateAuthToken, + }, + }; + }, [authTokens]); + + const onWalletAuthDeviceCallback = useCallback(() => { + setCurrentStep(WalletAuthLoginStep.validateSignature); + }, []); + + const deviceWalletAuth = useWalletAuthDevice( + onErrorCallback, + onWalletAuthDeviceCallback, + ); + + const onError = useCallback( + (e?: any) => { + logger.error('Error on useWalletAuth'); + logger.error(e); + deviceWalletAuth.cleanUp(); + onErrorCallback(e); + }, + [onErrorCallback, deviceWalletAuth.cleanUp], + ); + + const onLogin = useCallback( + (accessToken: string, refreshToken: string) => { + if (!walletIdRef.current) return; + + if (loginTypeRef.current === InheritanceUserTypeMap.owner) { + if (authTypeRef.current === 'seed-based') { + dispatch( + updateSeedAuthTokens({ + walletId: walletIdRef.current, + authTokens: { accessToken, refreshToken }, + }), + ); + } else { + dispatch( + updateWalletAuthTokens({ + walletId: walletIdRef.current, + authTokens: { accessToken, refreshToken }, + }), + ); + } + } + setAuthTokens({ accessToken, refreshToken }); + setCurrentStep(WalletAuthLoginStep.completed); + }, + [dispatch], + ); + + const startWalletAuth = useCallback(() => { + if (!walletIdRef.current || !initResponse.current || !authTypeRef.current) { + return false; + } + + deviceWalletAuth.startWalletAuth({ + walletId: walletIdRef.current, + challenge: initResponse.current.challenge, + isPublicKey: + initResponse.current.concern === InheritanceLoginConcernMap.REGISTER, + type: + initResponse.current.concern === InheritanceLoginConcernMap.REGISTER + ? 'seed-and-wallet-based' + : authTypeRef.current, + }); + + return true; + }, [deviceWalletAuth.startWalletAuth]); + + const fetchRequestIdCallback = useCallback( + async ( + walletId: string, + loginType: InheritanceUserType = InheritanceUserTypeMap.owner, + authType: 'seed-based' | 'wallet-based' = 'seed-based', + dontSkipLogin = false, + ) => { + try { + walletIdRef.current = walletId; + authTypeRef.current = authType; + loginTypeRef.current = loginType; + + const existingTokens = + authType === 'seed-based' + ? seedAuthTokensPerWallet[walletId] + : walletAuthTokensPerWallet[walletId]; + + if ( + !dontSkipLogin && + existingTokens && + loginType === InheritanceUserTypeMap.owner + ) { + const result = await inheritanceLoginService.refreshAccessToken({ + refreshToken: existingTokens.refreshToken, + }); + + const isLoggedOut = + result.error?.code === ServerErrorType.UNAUTHORIZED_ACCESS; + + if (result.result) { + onLogin(result.result.authToken, existingTokens.refreshToken); + return true; + } + + if (!isLoggedOut) { + throw result.error; + } + } + + const result = await inheritanceLoginService.init({ + walletId, + loginType, + authType: + authType === 'seed-based' + ? InheritanceLoginAuthTypeMap.seed + : InheritanceLoginAuthTypeMap.wallet, + }); + + if (result.error) { + throw result.error; + } + + initResponse.current = result.result; + setIsRegisterationRequired( + result.result.concern === InheritanceLoginConcernMap.REGISTER, + ); + + setCurrentStep(WalletAuthLoginStep.walletAuth); + return true; + } catch (error) { + onError(error); + + return false; + } + }, + [walletAuthTokensPerWallet, seedAuthTokensPerWallet, onLogin], + ); + + const [fetchRequestId, isFetchingRequestId] = useAsync( + fetchRequestIdCallback, + ); + + const validateSignatureCallback = useCallback(async () => { + try { + if ( + !walletIdRef.current || + !initResponse.current || + !deviceWalletAuth.deviceResponse.current + ) { + return false; + } + + const result = await inheritanceLoginService.validate({ + requestId: initResponse.current.requestId, + walletPublicKey: + deviceWalletAuth.deviceResponse.current.walletBased?.publicKey, + walletSignature: + deviceWalletAuth.deviceResponse.current.walletBased?.signature, + seedPublicKey: + deviceWalletAuth.deviceResponse.current.seedBased?.publicKey, + seedSignature: + deviceWalletAuth.deviceResponse.current.seedBased?.signature, + }); + + if (result.error) { + throw result.error; + } + + const prefillData = result.result.wallet?.owner; + if (prefillData) { + getPrefillData?.({ + name: prefillData.name ?? '', + email: prefillData.email ?? '', + alternateEmail: prefillData.alternateEmail ?? '', + }); + } + + if (result.result.otpDetails) { + setOtpVerificationDetails({ + id: 'emailVerificationOnLogin', + concern: OtpVerificationConcern.login, + emails: result.result.otpDetails.map(details => details.maskedEmail), + ...result.result.otpDetails[0], + }); + setCurrentStep(WalletAuthLoginStep.loginOtpVerify); + } else { + setCurrentStep(WalletAuthLoginStep.userDetails); + } + return true; + } catch (error) { + onError(error); + } + + return false; + }, []); + + const [validateSignature, isValidatingSignature] = useAsync( + validateSignatureCallback, + ); + + const registerUserCallback = useCallback(async (params: IUserDetails) => { + if (!initResponse.current) { + return false; + } + + const result = await inheritanceLoginService.register({ + requestId: initResponse.current.requestId, + name: params.name, + email: params.email, + alternateEmail: params.alternateEmail, + }); + + if (result.error) { + throw result.error; + } + + userDetails.current = params; + registerResponse.current = result.result; + + setOtpVerificationDetails({ + id: 'primaryVerificationOnRegister', + concern: OtpVerificationConcern.primary, + emails: [params.email], + ...result.result.otpDetails[0], + }); + setCurrentStep(WalletAuthLoginStep.primaryOtpVerify); + return true; + }, []); + + const [registerUser, isRegisteringUser] = useAsync( + registerUserCallback, + onError, + ); + + const onVerifyOtpCallback = useCallback( + async (otp: string) => { + if ( + !walletIdRef.current || + !initResponse.current || + !otpVerificationDetailsRef.current + ) { + return false; + } + + let result: + | ServerResponseWithError< + Pick< + Partial, + 'authToken' | 'refreshToken' + > + > + | undefined; + + if ( + otpVerificationDetailsRef.current.concern === + OtpVerificationConcern.login + ) { + result = await inheritanceLoginService.verify({ + requestId: initResponse.current.requestId, + otp, + }); + } else { + if (!registerResponse.current || !userDetails.current) { + return false; + } + + result = await inheritanceLoginService.registerVerify({ + requestId: initResponse.current.requestId, + otp, + emailType: + otpVerificationDetailsRef.current.concern === + OtpVerificationConcern.primary + ? InheritanceLoginEmailTypeMap.PRIMARY + : InheritanceLoginEmailTypeMap.ALTERNATE, + }); + } + + if (result.error) { + if (result.error.code === ServerErrorType.OTP_VERIFICATION_FAILED) { + setOtpVerificationDetails({ + ...otpVerificationDetailsRef.current, + showIncorrectError: true, + otpExpiry: + result.error.details?.responseBody.otpExpiry ?? + otpVerificationDetailsRef.current.otpExpiry, + retriesRemaining: + result.error.details?.responseBody.retriesRemaining ?? + otpVerificationDetailsRef.current.retriesRemaining, + }); + return false; + } + + throw result.error; + } + + if ( + otpVerificationDetailsRef.current.concern === + OtpVerificationConcern.primary + ) { + if (!registerResponse.current || !userDetails.current) { + return false; + } + + setOtpVerificationDetails({ + id: 'alternateVerificationOnRegister', + concern: OtpVerificationConcern.alternate, + emails: [userDetails.current.alternateEmail], + ...registerResponse.current.otpDetails[1], + }); + + setCurrentStep(WalletAuthLoginStep.alternateOtpVerify); + } else { + if (!result.result.authToken || !result.result.refreshToken) { + throw new Error('Server error'); + } + + setOtpVerificationDetails(undefined); + onLogin(result.result.authToken, result.result.refreshToken); + } + + return true; + }, + [onLogin], + ); + + const [verifyOtp, isVerifyingOtp] = useAsync(onVerifyOtpCallback, onError); + + const reset = useCallback(() => { + walletIdRef.current = undefined; + initResponse.current = undefined; + userDetails.current = undefined; + registerResponse.current = undefined; + setIsRegisterationRequired(false); + setAuthTokens(undefined); + setOtpVerificationDetails(undefined); + deviceWalletAuth.reset(); + setCurrentStep(WalletAuthLoginStep.fetchRequestId); + }, [deviceWalletAuth.reset]); + + return useMemoReturn({ + fetchRequestId, + isFetchingRequestId, + startWalletAuth, + deviceEvents: deviceWalletAuth.deviceEvents, + validateSignature, + isValidatingSignature, + reset, + currentStep, + registerUser, + isRegisteringUser, + abortWalletAuth: deviceWalletAuth.cleanUp, + otpVerificationDetails, + authTokens, + verifyOtp, + isVerifyingOtp, + isRegisterationRequired, + authTokenConfig, + }); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/hooks/useWalletAuth/types.ts b/packages/cysync-core/src/dialogs/Inheritance/hooks/useWalletAuth/types.ts new file mode 100644 index 000000000..be5ee9f16 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/hooks/useWalletAuth/types.ts @@ -0,0 +1,34 @@ +export interface IUserDetails { + name: string; + email: string; + alternateEmail: string; +} + +export enum OtpVerificationConcern { + primary = 'primary', + alternate = 'alternate', + login = 'login', +} + +export interface IOtpVerificationDetails { + id: string; + concern: OtpVerificationConcern; + emails: string[]; + retriesRemaining: number; + otpExpiry: string; + showIncorrectError?: boolean; +} + +export const WalletAuthLoginStep = { + fetchRequestId: 0, + walletAuth: 1, + validateSignature: 2, + userDetails: 3, + primaryOtpVerify: 4, + alternateOtpVerify: 5, + loginOtpVerify: 6, + completed: 7, +} as const; + +export type WalletAuthLoginStep = + (typeof WalletAuthLoginStep)[keyof typeof WalletAuthLoginStep]; diff --git a/packages/cysync-core/src/dialogs/Inheritance/hooks/useWalletAuth/useWalletAuthDevice.ts b/packages/cysync-core/src/dialogs/Inheritance/hooks/useWalletAuth/useWalletAuthDevice.ts new file mode 100644 index 000000000..89229c4a1 --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/hooks/useWalletAuth/useWalletAuthDevice.ts @@ -0,0 +1,128 @@ +import { + IInheritanceWalletAuthEvent, + IInheritanceWalletAuthParams, +} from '@cypherock/app-support-inheritance'; +import lodash from 'lodash'; +import { useCallback, useMemo, useRef, useState } from 'react'; +import { Observer, Subscription } from 'rxjs'; + +import { deviceLock, useDevice } from '~/context'; +import { inheritanceSupport } from '~/utils'; +import logger from '~/utils/logger'; + +export const useWalletAuthDevice = ( + onErrorCallback: (e?: any) => void, + onSuccessCallback: () => void, +) => { + const { connection } = useDevice(); + + const [deviceEvents, setDeviceEvents] = useState< + Record + >({}); + const deviceResponse = useRef< + | { + walletBased?: { publicKey?: string; signature: string }; + seedBased?: { publicKey?: string; signature: string }; + } + | undefined + >(); + const flowSubscription = useRef(); + + const cleanUp = useCallback(() => { + if (flowSubscription.current) { + flowSubscription.current.unsubscribe(); + flowSubscription.current = undefined; + } + }, []); + + const onError = useCallback( + (e?: any) => { + logger.error('Error on useWalletAuthDevice'); + logger.error(e); + cleanUp(); + onErrorCallback(e); + }, + [onErrorCallback, cleanUp], + ); + + const getFlowObserver = useCallback( + (onEnd: () => void): Observer => ({ + next: payload => { + if (payload.device) setDeviceEvents({ ...payload.device.events }); + if (payload.walletBased) { + deviceResponse.current = { + ...(deviceResponse.current ?? {}), + walletBased: payload.walletBased, + }; + } + if (payload.seedBased) { + deviceResponse.current = { + ...(deviceResponse.current ?? {}), + seedBased: payload.seedBased, + }; + } + }, + error: err => { + onEnd(); + onError(err); + }, + complete: () => { + onSuccessCallback(); + cleanUp(); + onEnd(); + }, + }), + [onError], + ); + + const startWalletAuth = useCallback( + async (params: Omit) => { + logger.info('Starting wallet auth'); + + if (!connection?.connection) { + return; + } + + try { + cleanUp(); + + const taskId = lodash.uniqueId('task-'); + + await deviceLock.acquire(connection.device, taskId); + + const onEnd = () => { + deviceLock.release(connection.device, taskId); + }; + + const deviceConnection = connection.connection; + + flowSubscription.current = inheritanceSupport + .walletAuth({ + connection: deviceConnection, + ...params, + }) + .subscribe(getFlowObserver(onEnd)); + } catch (e) { + onError(e); + } + }, + [connection], + ); + + const reset = useCallback(() => { + deviceResponse.current = undefined; + setDeviceEvents({}); + cleanUp(); + }, [cleanUp]); + + return useMemo( + () => ({ + cleanUp, + deviceResponse, + startWalletAuth, + deviceEvents, + reset, + }), + [cleanUp, startWalletAuth, deviceEvents, reset, deviceResponse], + ); +}; diff --git a/packages/cysync-core/src/dialogs/Inheritance/index.ts b/packages/cysync-core/src/dialogs/Inheritance/index.ts new file mode 100644 index 000000000..6f266eebd --- /dev/null +++ b/packages/cysync-core/src/dialogs/Inheritance/index.ts @@ -0,0 +1,10 @@ +export * from './SyncPlans'; +export * from './PlanLogin'; +export * from './SilverPlanPurchase'; +export * from './EditExecutorMessage'; +export * from './EditReminderTime'; +export * from './EditUserDetails'; +export * from './GoldPlanPurchase'; +export * from './EditEncryptedMessage'; +export * from './PinRecovery'; +export * from './EstateRecovery'; diff --git a/packages/cysync-core/src/dialogs/index.tsx b/packages/cysync-core/src/dialogs/index.tsx index 2b40cc3ff..faa0e5b7b 100644 --- a/packages/cysync-core/src/dialogs/index.tsx +++ b/packages/cysync-core/src/dialogs/index.tsx @@ -19,6 +19,18 @@ import { EditAccountDialog } from './EditAccountDialog'; import { ErrorDialog } from './ErrorDialog'; import { GuidedFlow } from './GuidedFlow'; import { HistoryDialog } from './HistoryDialog'; +import { + InheritanceEditExecutorMessageDialog, + InheritanceEditReminderTimeDialog, + InheritanceEditUserDetailsDialog, + InheritancePlanLoginDialog, + InheritanceSyncPlansDialog, + InheritanceSilverPlanPurchaseDialog, + InheritanceGoldPlanPurchaseDialog, + InheritanceEditEncryptedMessageDialog, +} from './Inheritance'; +import { InheritanceEstateRecoveryDialog } from './Inheritance/EstateRecovery'; +import { InheritancePinRecoveryDialog } from './Inheritance/PinRecovery'; import { ChangePasswordDialog, RemovePasswordDialog, @@ -57,4 +69,14 @@ export const dialogs: Record = { contactSupportDialog: ContactSupportDialog, deleteAccount: DeleteAccountDialog, betaNotificationDialog: BetaNotificationDialog, + inheritanceSyncPlans: InheritanceSyncPlansDialog, + inheritancePlanLogin: InheritancePlanLoginDialog, + inheritanceSilverPlanPurchase: InheritanceSilverPlanPurchaseDialog, + inheritanceEditExecutorMessage: InheritanceEditExecutorMessageDialog, + inheritanceEditReminderTime: InheritanceEditReminderTimeDialog, + inheritanceEditUserDetails: InheritanceEditUserDetailsDialog, + inheritancePinRecovery: InheritancePinRecoveryDialog, + inheritanceGoldPlanPurchase: InheritanceGoldPlanPurchaseDialog, + inheritanceEditEncryptedMessage: InheritanceEditEncryptedMessageDialog, + inheritanceEstateRecovery: InheritanceEstateRecoveryDialog, }; diff --git a/packages/cysync-core/src/hooks/index.ts b/packages/cysync-core/src/hooks/index.ts index a5fc13141..f26357028 100644 --- a/packages/cysync-core/src/hooks/index.ts +++ b/packages/cysync-core/src/hooks/index.ts @@ -20,3 +20,7 @@ export * from './useGraph'; export * from './useAssetDropdown'; export * from './useSubAccounts'; export * from './useWalletSync'; +export * from './useCountdown'; +export * from './useAsync'; +export * from './useMemoReturn'; +export * from './useCallbackAfterCountdown'; diff --git a/packages/cysync-core/src/hooks/useAsync.ts b/packages/cysync-core/src/hooks/useAsync.ts new file mode 100644 index 000000000..99baa1a3d --- /dev/null +++ b/packages/cysync-core/src/hooks/useAsync.ts @@ -0,0 +1,38 @@ +import { useCallback, useState } from 'react'; + +export function useAsync( + callback: (...params: T) => Promise, + onError?: (e?: any) => void, +) { + const [isLoading, setIsLoading] = useState(false); + const [isCompleted, setIsCompleted] = useState(false); + + const run = useCallback( + async (...params: T) => { + setIsLoading(true); + + let result: boolean | undefined; + try { + result = await callback(...params); + setIsCompleted(true); + setIsLoading(false); + } catch (error) { + setIsLoading(false); + if (onError) { + onError(error); + } + result = false; + } + + return result; + }, + [callback, onError], + ); + + const reset = useCallback(() => { + setIsLoading(false); + setIsCompleted(false); + }, []); + + return [run, isLoading, isCompleted, reset] as const; +} diff --git a/packages/cysync-core/src/hooks/useCallbackAfterCountdown.tsx b/packages/cysync-core/src/hooks/useCallbackAfterCountdown.tsx new file mode 100644 index 000000000..4d7557edd --- /dev/null +++ b/packages/cysync-core/src/hooks/useCallbackAfterCountdown.tsx @@ -0,0 +1,44 @@ +import { useEffect, useRef } from 'react'; + +export const useCallbackAfterCountdown = ( + callback: () => void, + countdown: number, +) => { + const timeout = useRef(); + const callbackRef = useRef(callback); + const startTimeRef = useRef(); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + const start = () => { + const timePassed = startTimeRef.current + ? Date.now() - startTimeRef.current + : 0; + + if (timePassed < countdown) { + timeout.current = setTimeout(() => { + callbackRef.current(); + }, countdown - timePassed); + } else { + callbackRef.current(); + } + }; + + const stop = () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + }; + + const setStartTime = (time?: number) => { + startTimeRef.current = time; + }; + + return { + setStartTime, + start, + stop, + }; +}; diff --git a/packages/cysync-core/src/hooks/useCountdown.tsx b/packages/cysync-core/src/hooks/useCountdown.tsx new file mode 100644 index 000000000..621efffdd --- /dev/null +++ b/packages/cysync-core/src/hooks/useCountdown.tsx @@ -0,0 +1,35 @@ +import { useEffect, useRef, useState } from 'react'; + +export const useCountdown = (time: number) => { + const timeRef = useRef(time); + + useEffect(() => { + timeRef.current = time; + }, [time]); + + const [state, setState] = useState({ + seconds: 0, + }); + + useEffect(() => { + const interval = setInterval(() => updateCountdown(), 1000); + + return () => clearInterval(interval); + }, []); + + const updateCountdown = () => { + const countdownDate = timeRef.current; + + if (countdownDate) { + const currentTime = new Date().getTime(); + + const distanceToDate = countdownDate - currentTime; + + const seconds = Math.floor(distanceToDate / 1000); + + setState({ seconds }); + } + }; + + return state; +}; diff --git a/packages/cysync-core/src/hooks/useErrorHandler.ts b/packages/cysync-core/src/hooks/useErrorHandler.ts index 2c07c9593..dbe0f8f23 100644 --- a/packages/cysync-core/src/hooks/useErrorHandler.ts +++ b/packages/cysync-core/src/hooks/useErrorHandler.ts @@ -51,7 +51,7 @@ export const useErrorHandler = (params: IErrorHandlerParams) => { const errorMsg = React.useMemo(() => { if (error) { - logger.error(error); + logger.error(JSON.stringify(error, Object.getOwnPropertyNames(error))); } const parsedError = error diff --git a/packages/cysync-core/src/hooks/useMemoReturn.tsx b/packages/cysync-core/src/hooks/useMemoReturn.tsx new file mode 100644 index 000000000..8c923368c --- /dev/null +++ b/packages/cysync-core/src/hooks/useMemoReturn.tsx @@ -0,0 +1,5 @@ +import { useMemo } from 'react'; + +export function useMemoReturn>(params: T) { + return useMemo(() => params, [...Object.values(params)]); +} diff --git a/packages/cysync-core/src/hooks/useTabsAndDialogs.ts b/packages/cysync-core/src/hooks/useTabsAndDialogs.ts index eb0b98789..46be0acc3 100644 --- a/packages/cysync-core/src/hooks/useTabsAndDialogs.ts +++ b/packages/cysync-core/src/hooks/useTabsAndDialogs.ts @@ -28,7 +28,6 @@ export function useTabsAndDialogs({ }: IUseTabsAndDialogs) { const [currentTab, setCurrentTab] = useState(defaultTab); const [currentDialog, setCurrentDialog] = useState(defaultDialog); - const [isDeviceRequired, setIsDeviceRequired] = useState(false); const checkIfDeviceRequiredInDialog = useCallback( (tab: number, dialog: number) => { @@ -40,6 +39,10 @@ export function useTabsAndDialogs({ [deviceRequiredDialogsMap], ); + const [isDeviceRequired, setIsDeviceRequired] = useState( + checkIfDeviceRequiredInDialog(defaultTab, defaultDialog), + ); + const goTo = useCallback( (tab: number, dialog = 0) => { const _isDeviceRequired = checkIfDeviceRequiredInDialog(tab, dialog); @@ -75,6 +78,8 @@ export function useTabsAndDialogs({ } else { goTo(newTab); } + + return newTab; }, [currentTab, tabs, goTo]); const goToNextDialog = useCallback(() => { @@ -84,30 +89,33 @@ export function useTabsAndDialogs({ ); goTo(currentTab, nextDialog); + return nextDialog; }, [currentDialog, currentTab, tabs, goTo]); - const onNext = useCallback(() => { + const onNext = useCallback((): [number, number] => { if (currentDialog + 1 > tabs[currentTab].dialogs.length - 1) { - goToNextTab(); - } else { - goToNextDialog(); + const tab = goToNextTab(); + return [tab, 0]; } + const dialog = goToNextDialog(); + return [currentTab, dialog]; }, [currentDialog, currentTab, tabs, goToNextTab, goToNextDialog]); - const onPrevious = useCallback(() => { + const onPrevious = useCallback((): [number, number] => { if (currentDialog - 1 < 0) { if (currentTab === 0) { goTo(currentTab, 0); - } else { - const nextDialog = tabs[currentTab - 1].dialogs.length - 1; - const nextTab = Math.max(0, currentTab - 1); - - goTo(nextTab, nextDialog); + return [currentTab, 0]; } - } else { - const nextDialog = Math.max(0, currentDialog - 1); - goTo(currentTab, nextDialog); + const nextDialog = tabs[currentTab - 1].dialogs.length - 1; + const nextTab = Math.max(0, currentTab - 1); + + goTo(nextTab, nextDialog); + return [nextTab, nextDialog]; } + const nextDialog = Math.max(0, currentDialog - 1); + goTo(currentTab, nextDialog); + return [currentTab, nextDialog]; }, [currentDialog, currentTab, tabs, goTo]); return { diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/ChoosePlan.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/ChoosePlan.tsx new file mode 100644 index 000000000..5acd61096 --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/ChoosePlan.tsx @@ -0,0 +1,88 @@ +import { + Typography, + Container, + Flex, + PlanCard, + Button, + ArrowBackGoldenIcon, +} from '@cypherock/cysync-ui'; +import React, { FC, useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; + +import { + openInheritanceGoldPlanPurchaseDialog, + openInheritanceSilverPlanPurchaseDialog, +} from '~/actions'; +import { selectLanguage, useAppSelector } from '~/store'; + +import { InheritancePageLayout } from './Layout'; + +export const InheritanceChoosePlan: FC = () => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritance.choosePlan; + const navigate = useNavigate(); + const dispatch = useDispatch(); + + const SilverPlanFeatures = strings.plans.features.map((cur, i) => ({ + text: cur, + available: i < 3, + })); + + const GoldPlanFeatures = strings.plans.features.map(cur => ({ + text: cur, + available: true, + })); + + const onBack = useCallback(() => { + navigate(-1); + }, [navigate]); + + const openSilverPlanSetup = useCallback(() => { + dispatch(openInheritanceSilverPlanPurchaseDialog()); + }, [dispatch]); + + const openGoldPlanSetup = useCallback(() => { + dispatch(openInheritanceGoldPlanPurchaseDialog()); + }, [dispatch]); + + return ( + + + + + + + + + Sync Icon + + + {lang.strings.inheritance.homePage.setup.syncFromMail.title} + + + { + lang.strings.inheritance.homePage.setup.syncFromMail + .subTitle + } + + + + + + + + + + + + question mark + + + {lang.strings.inheritance.homePage.setup.learnMore.title} + + + {lang.strings.inheritance.homePage.setup.learnMore.subTitle} + + + + + + + + + + ); +}; diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/Home/index.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/Home/index.tsx new file mode 100644 index 000000000..0395a37cb --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/Home/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import { selectInheritancePlans, useAppSelector } from '~/store'; + +import { InheritancePlanList } from './PlanList'; +import { InheritanceSetup } from './Setup'; + +export const InheritanceHome = () => { + const inheritancePlans = useAppSelector(selectInheritancePlans); + + if (inheritancePlans.length <= 0) { + return ; + } + + return ; +}; diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/Layout.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/Layout.tsx new file mode 100644 index 000000000..1b8017863 --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/Layout.tsx @@ -0,0 +1,49 @@ +import { Button, Container, Flex, LangDisplay } from '@cypherock/cysync-ui'; +import React, { FC, ReactNode } from 'react'; + +import { useAppSelector, selectLanguage } from '~/store'; + +import { MainAppLayout } from '../Layout'; + +interface InheritancePageLayoutProps { + children?: ReactNode; + actionButtonText?: string; + onActionButtonClick?: () => void; +} + +export const InheritancePageLayout: FC = ({ + children, + onActionButtonClick, + actionButtonText, +}) => { + const lang = useAppSelector(selectLanguage); + + return ( + + + {onActionButtonClick && actionButtonText && ( + + + + )} + + {children} + + + + ); +}; + +InheritancePageLayout.defaultProps = { + children: undefined, + onActionButtonClick: undefined, + actionButtonText: undefined, +}; diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/Details.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/Details.tsx new file mode 100644 index 000000000..657d17fc0 --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/Details.tsx @@ -0,0 +1,124 @@ +import { format as formatDate } from 'date-fns'; +import React, { FC, useCallback, useMemo } from 'react'; + +import { selectLanguage, useAppDispatch, useAppSelector } from '~/store'; + +import { InheritancePlanDetailsSectionProps } from './types'; + +import { + InheritancePlanData, + InheritancePlanDetailsGrid, + InheritancePlanDetailsLayout, + UserDetails, +} from '../components'; +import { + openInheritanceEditEncryptedMessageDialog, + openInheritanceEditExecutorMessageDialog, + openInheritanceEditReminderTimeDialog, + openInheritanceEditUserDetailsDialog, +} from '~/actions'; + +const planDetailsObjectKeyMapper = (obj: { + name: string; + email: string; + alternateEmail: string; +}) => ({ + name: obj.name, + primaryEmail: obj.email, + secondaryEmail: obj.alternateEmail, +}); + +export const InheritancePlanDetailsSection: FC< + InheritancePlanDetailsSectionProps +> = ({ onBack, plan, planDetails }) => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritance; + const dispatch = useAppDispatch(); + + const editReminder = useCallback(() => { + dispatch( + openInheritanceEditReminderTimeDialog({ walletId: plan.walletId }), + ); + }, [dispatch]); + + const editOwnerDetails = useCallback(() => { + dispatch(openInheritanceEditUserDetailsDialog({ userType: 'owner' })); + }, [dispatch]); + + const editNomineeDetails = useCallback(() => { + dispatch(openInheritanceEditUserDetailsDialog({ userType: 'nominee' })); + }, [dispatch]); + + const editExecutorDetails = useCallback(() => { + dispatch(openInheritanceEditUserDetailsDialog({ userType: 'executor' })); + }, [dispatch]); + + const editExecutorMessage = useCallback(() => { + dispatch( + openInheritanceEditExecutorMessageDialog({ walletId: plan.walletId }), + ); + }, [dispatch, plan]); + + const editEncryptedMessage = useCallback(() => { + dispatch(openInheritanceEditEncryptedMessageDialog()); + }, [dispatch]); + + const data = useMemo(() => { + const wallet = { + walletName: planDetails.name, + createdOn: formatDate(planDetails.activationDate, 'dd MMMM yyyy'), + expiringOn: formatDate(planDetails.expiryDate, 'dd MMMM yyyy'), + onEdit: editReminder, + }; + + if (planDetails.reminderPeriod) + Object.assign(wallet, { reminderPeriod: planDetails.reminderPeriod }); + + const owner = { + ...planDetailsObjectKeyMapper(planDetails.owner), + onEdit: editOwnerDetails, + }; + + let nominees: UserDetails[] | undefined; + if (planDetails.nominee.length > 0) + nominees = planDetails.nominee.map(n => ({ + ...planDetailsObjectKeyMapper(n), + onEdit: editNomineeDetails, + onSecondaryEdit: editEncryptedMessage, + })); + + let executor: UserDetails | undefined; + if (planDetails.executor) + executor = { + ...planDetailsObjectKeyMapper(planDetails.executor), + onEdit: editExecutorDetails, + onSecondaryEdit: editExecutorMessage, + }; + + return { + wallet, + owner, + nominees, + executor, + }; + }, [ + planDetails, + plan, + editEncryptedMessage, + editExecutorMessage, + editNomineeDetails, + editExecutorDetails, + editOwnerDetails, + editReminder, + ]); + + return ( + + + + ); +}; diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/LockedPlan.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/LockedPlan.tsx new file mode 100644 index 000000000..ef631a8b9 --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/LockedPlan.tsx @@ -0,0 +1,123 @@ +import { Container, Button, Lock, svgGradients } from '@cypherock/cysync-ui'; +import React, { FC } from 'react'; + +import { selectLanguage, useAppSelector } from '~/store'; + +import { InheritanceLockedPlanSectionProps } from './types'; + +import { + InheritancePlanDetailsGrid, + InheritancePlanDetailsLayout, +} from '../components'; + +export const InheritanceLockedPlan: FC = ({ + plan, + onBack, + onUnlock, +}) => { + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritance; + + const data = { + wallet: { + walletName: 'MyFunnyWallet', + createdOn: '01 July 2024', + expiringOn: '30 June 2024', + reminderPeriod: 1, + onEdit: () => { + alert('Edit reminder period clicked!!'); + }, + }, + owner: { + name: 'Alfred Bellows', + primaryEmail: 'doc.bellows@yahoo.com', + secondaryEmail: 'alfred@psych.com', + onEdit: () => { + alert('Edit owner details clicked!!'); + }, + }, + nominees: [ + { + name: 'Alfred Bellows', + primaryEmail: 'doc.bellows@yahoo.com', + secondaryEmail: 'alfred@psych.com', + onEdit: () => { + alert('Edit nominee details clicked!!'); + }, + onSecondaryEdit: () => { + alert('Edit encrypted message clicked!!'); + }, + }, + { + name: 'Jane Doe', + primaryEmail: 'jane.doe@example.com', + secondaryEmail: 'jane@another.com', + onEdit: () => { + alert('Edit nominee details clicked!!'); + }, + onSecondaryEdit: () => { + alert('Edit encrypted message clicked!!'); + }, + }, + ], + executor: { + name: 'John Smith', + primaryEmail: 'john.smith@example.com', + secondaryEmail: 'john@other.com', + onEdit: () => { + alert('Edit executor details clicked!!'); + }, + onSecondaryEdit: () => { + alert('Edit executor message clicked!!'); + }, + }, + }; + + return ( + + +
+ +
+
+ + + + + + +
+
+
+ ); +}; diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/index.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/index.tsx new file mode 100644 index 000000000..b293b3f9c --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/index.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; + +import { InheritancePlanDetailsSection } from './Details'; +import { InheritanceLockedPlan } from './LockedPlan'; + +import { useInheritancePlanPage } from '../hooks'; + +export const InheritancePlanDetails: FC = () => { + const { plan, planDetails, onBack, onUnlock } = useInheritancePlanPage(); + + if (!plan) return null; + + if (!planDetails) + return ( + + ); + + return ( + + ); +}; diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/types.ts b/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/types.ts new file mode 100644 index 000000000..d2287b3d4 --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/PlanDetails/types.ts @@ -0,0 +1,15 @@ +import { IInheritancePlan } from '@cypherock/db-interfaces'; + +import { IInheritancePlanDetails } from '~/store'; + +export interface InheritancePlanDetailsSectionProps { + onBack: () => void; + plan: IInheritancePlan; + planDetails: IInheritancePlanDetails; +} + +export interface InheritanceLockedPlanSectionProps { + onBack: () => void; + plan: IInheritancePlan; + onUnlock: () => void; +} diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/components/PlanDetailsGrid.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/components/PlanDetailsGrid.tsx new file mode 100644 index 000000000..a15f2b537 --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/components/PlanDetailsGrid.tsx @@ -0,0 +1,230 @@ +import { + ClockIcon, + DetailsCard, + EditButton, + EmailIconSmall, + EncryptedMessageIcon, + parseLangTemplate, + svgGradients, + Typography, + UserIcon, + WalletIcon, +} from '@cypherock/cysync-ui'; +import { + InheritancePlanType, + InheritancePlanTypeMap, +} from '@cypherock/db-interfaces'; +import React from 'react'; + +import { ILangState } from '~/store'; + +interface Wallet { + walletName: string; + createdOn: string; + expiringOn: string; + reminderPeriod?: number; + onEdit: () => void; +} + +export interface UserDetails { + name: string; + primaryEmail: string; + secondaryEmail?: string; + onEdit: () => void; + onSecondaryEdit?: () => void; +} + +export interface InheritancePlanData { + wallet: Wallet; + owner: UserDetails; + nominees?: UserDetails[]; + executor?: UserDetails; +} + +export interface PlanDetailsGridProps { + data: InheritancePlanData; + strings: ILangState['strings']['inheritance']; + planType: InheritancePlanType; +} + +const goldWalletIcon = ; + +export const InheritancePlanDetailsGrid: React.FC = ({ + data, + strings, + planType, +}) => { + const getReminderPeriodInputText = (newPeriod: number) => { + const reminderPeriodInput = + strings.planDetails.walletDetails.reminderPeriodField; + return parseLangTemplate( + newPeriod === 1 + ? reminderPeriodInput.input + : reminderPeriodInput.inputPlural, + { month: newPeriod }, + ); + }; + + return ( +
+ + {strings.plans[planType].title.toLocaleUpperCase()} + + } + $backgroundType={ + planType === InheritancePlanTypeMap.gold ? 'gold' : 'silver' + } + fields={[ + { + label: strings.planDetails.walletDetails.createdOn, + icon: ClockIcon, + value: data.wallet.createdOn, + }, + { + label: strings.planDetails.walletDetails.expiringOn, + icon: ClockIcon, + value: data.wallet.expiringOn, + isDanger: true, + }, + ...(data.wallet.reminderPeriod + ? [ + { + label: + strings.planDetails.walletDetails.reminderPeriodField.label, + icon: ClockIcon, + value: getReminderPeriodInputText(data.wallet.reminderPeriod), + trailing: ( + + ), + }, + ] + : []), + ]} + /> + } + fields={[ + { + label: strings.planDetails.ownerDetails.form.userNameField.label, + icon: UserIcon, + value: data.owner.name, + }, + { + label: + strings.planDetails.ownerDetails.form.primaryEmailField.label, + icon: EmailIconSmall, + value: data.owner.primaryEmail, + }, + { + label: + strings.planDetails.ownerDetails.form.secondaryEmailField.label, + icon: EmailIconSmall, + value: data.owner.secondaryEmail, + }, + ]} + /> + {planType === InheritancePlanTypeMap.gold && + data.nominees?.map((nominee, index) => ( + } + fields={[ + { + label: + strings.planDetails.nomineeDetails.form.nomineeNameField + .label, + icon: UserIcon, + value: nominee.name, + }, + { + label: + strings.planDetails.nomineeDetails.form.primaryEmailField + .label, + icon: EmailIconSmall, + value: nominee.primaryEmail, + }, + ...(nominee.secondaryEmail + ? [ + { + label: + strings.planDetails.nomineeDetails.form + .secondaryEmailField.label, + icon: EmailIconSmall, + value: nominee.secondaryEmail, + }, + ] + : []), + ]} + footer={{ + label: + strings.planDetails.nomineeDetails.form.encryptedMessage.label, + icon: EncryptedMessageIcon, + trailing: ( + + ), + }} + /> + ))} + {planType === InheritancePlanTypeMap.gold && data.executor && ( + + } + fields={[ + { + label: + strings.planDetails.executorDetails.form.nomineeNameField.label, + icon: UserIcon, + value: data.executor.name, + }, + { + label: + strings.planDetails.executorDetails.form.primaryEmailField + .label, + icon: EmailIconSmall, + value: data.executor.primaryEmail, + }, + ...(data.executor.secondaryEmail + ? [ + { + label: + strings.planDetails.executorDetails.form + .secondaryEmailField.label, + icon: EmailIconSmall, + value: data.executor.secondaryEmail, + }, + ] + : []), + ]} + footer={{ + label: + strings.planDetails.executorDetails.form.executorMessage.label, + icon: EncryptedMessageIcon, + trailing: ( + + ), + }} + /> + )} +
+ ); +}; diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/components/PlanDetailsLayout.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/components/PlanDetailsLayout.tsx new file mode 100644 index 000000000..190b2d43c --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/components/PlanDetailsLayout.tsx @@ -0,0 +1,68 @@ +import { + ArrowBackGoldenIcon, + Button, + Container, + Flex, +} from '@cypherock/cysync-ui'; +import { IInheritancePlan } from '@cypherock/db-interfaces'; +import React, { FC } from 'react'; + +import { openInheritancePinRecoveryDialog } from '~/actions'; +import { selectLanguage, useAppDispatch, useAppSelector } from '~/store'; + +import { InheritancePageLayout } from '../Layout'; + +interface InheritancePageLayoutProps { + children: React.ReactNode; + onBack: () => void; + plan: IInheritancePlan; +} + +export const InheritancePlanDetailsLayout: FC = ({ + children, + plan, + onBack, +}) => { + const dispatch = useAppDispatch(); + const lang = useAppSelector(selectLanguage); + const strings = lang.strings.inheritance; + + const onRenewPlan = () => { + // TODO: Implement renewal logic here + alert('Renewal Clicked'); + }; + + // TODO: Add upgrade plan logic here + + const onRecoverPin = () => { + dispatch( + openInheritancePinRecoveryDialog({ + walletId: plan.walletId, + walletName: plan.walletName, + }), + ); + }; + + return ( + + + + + + + + {children} + + + ); +}; diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/components/index.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/components/index.tsx new file mode 100644 index 000000000..056caba35 --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/components/index.tsx @@ -0,0 +1,2 @@ +export * from './PlanDetailsGrid'; +export * from './PlanDetailsLayout'; diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/hooks/index.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/hooks/index.tsx new file mode 100644 index 000000000..f171a092d --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/hooks/index.tsx @@ -0,0 +1 @@ +export * from './useInheritancePlanPage'; diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/hooks/useInheritancePlanPage.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/hooks/useInheritancePlanPage.tsx new file mode 100644 index 000000000..80d04516e --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/hooks/useInheritancePlanPage.tsx @@ -0,0 +1,64 @@ +import { useCallback, useEffect, useMemo } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { openInheritancePlanLoginDialog } from '~/actions'; +import { useQuery, useMemoReturn } from '~/hooks'; +import { + useAppSelector, + selectInheritancePlans, + useAppDispatch, + clearInheritancePlanDetails, +} from '~/store'; + +export const useInheritancePlanPage = () => { + const navigate = useNavigate(); + const dispatch = useAppDispatch(); + const query = useQuery(); + const plans = useAppSelector(selectInheritancePlans); + + const allPlanDetails = useAppSelector( + state => state.inheritance.inheritancePlanDetails, + ); + + const onBack = useCallback(() => { + navigate(-1); + }, [navigate]); + + const plan = useMemo(() => { + const walletId = query.get('walletId'); + if (!walletId) return undefined; + + return plans.find(p => p.walletId.toLowerCase() === walletId.toLowerCase()); + }, [plans, query]); + + const planDetails = useMemo(() => { + if (!plan) return undefined; + + return allPlanDetails[plan.walletId]; + }, [allPlanDetails, plan]); + + const onUnlock = useCallback(() => { + if (!plan) return; + + dispatch( + openInheritancePlanLoginDialog({ + walletId: plan.walletId, + walletName: plan.walletName, + }), + ); + }, [plan]); + + useEffect( + () => () => { + dispatch(clearInheritancePlanDetails); + }, + [], + ); + + return useMemoReturn({ + plan, + onBack, + onUnlock, + planDetails, + }); +}; diff --git a/packages/cysync-core/src/pages/MainApp/Inheritance/index.tsx b/packages/cysync-core/src/pages/MainApp/Inheritance/index.tsx new file mode 100644 index 000000000..095538566 --- /dev/null +++ b/packages/cysync-core/src/pages/MainApp/Inheritance/index.tsx @@ -0,0 +1,3 @@ +export * from './ChoosePlan'; +export * from './Home'; +export * from './PlanDetails'; diff --git a/packages/cysync-core/src/pages/MainApp/Layout.tsx b/packages/cysync-core/src/pages/MainApp/Layout.tsx index 63a437cd6..2d5c12c34 100644 --- a/packages/cysync-core/src/pages/MainApp/Layout.tsx +++ b/packages/cysync-core/src/pages/MainApp/Layout.tsx @@ -9,6 +9,7 @@ import { TopbarProps, Notification, } from '~/components'; +import { FeatureBanner } from '~/components/FeatureBanner'; interface MainAppLayoutProps { topbar: TopbarProps; @@ -52,6 +53,7 @@ const MainAppLayoutComponent: FC = ({ + diff --git a/packages/cysync-core/src/pages/MainApp/index.ts b/packages/cysync-core/src/pages/MainApp/index.ts index 8fad3948b..b4d5c12e2 100644 --- a/packages/cysync-core/src/pages/MainApp/index.ts +++ b/packages/cysync-core/src/pages/MainApp/index.ts @@ -5,3 +5,4 @@ export * from './Settings'; export * from './Help'; export * from './Asset'; export * from './Account'; +export * from './Inheritance'; diff --git a/packages/cysync-core/src/pages/OnBoarding/Terms.tsx b/packages/cysync-core/src/pages/OnBoarding/Terms.tsx index 3da7d074e..067ae11bc 100644 --- a/packages/cysync-core/src/pages/OnBoarding/Terms.tsx +++ b/packages/cysync-core/src/pages/OnBoarding/Terms.tsx @@ -1,5 +1,4 @@ import { - Bullet, Button, CheckBox, Container, @@ -7,12 +6,11 @@ import { DialogBoxBody, DialogBoxFooter, Flex, - Image, LangDisplay, LogoOutlinedAsideImage, - openExternalLink, TermsOfUseGraphics, Typography, + ExternalLink, } from '@cypherock/cysync-ui'; import React, { Dispatch, @@ -30,42 +28,6 @@ import { keyValueStore } from '~/utils'; import { OnboardingPageLayout } from './OnboardingPageLayout'; -const ExternalLinkItem: React.FC<{ - text: string; - href: string; -}> = ({ text, href }) => ( - - - - - - - - - - termsLink - - - -); - const TermsDialogBox: FC<{ isChecked: boolean; setIsChecked: Dispatch>; @@ -105,11 +67,11 @@ const TermsDialogBox: FC<{
- - diff --git a/packages/cysync-core/src/services/index.ts b/packages/cysync-core/src/services/index.ts index 9c002d79c..86d727e3a 100644 --- a/packages/cysync-core/src/services/index.ts +++ b/packages/cysync-core/src/services/index.ts @@ -1 +1,2 @@ export * from './feedbackService'; +export * from './inheritance'; diff --git a/packages/cysync-core/src/services/inheritance/common.ts b/packages/cysync-core/src/services/inheritance/common.ts new file mode 100644 index 000000000..73f42aaa6 --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/common.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +export const inheritanceBaseUrl = 'https://api-inheritance-dev.cypherock.com'; + +export const otpDetailSchema = z.object({ + maskedEmail: z.string(), + retriesRemaining: z.number(), + otpExpiry: z.string().datetime(), +}); + +export const InheritanceUserTypeMap = { + owner: 'OWNER', + nominee: 'NOMINEE', +} as const; + +export type InheritanceUserType = + (typeof InheritanceUserTypeMap)[keyof typeof InheritanceUserTypeMap]; diff --git a/packages/cysync-core/src/services/inheritance/index.ts b/packages/cysync-core/src/services/inheritance/index.ts new file mode 100644 index 000000000..14fb45d9a --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/index.ts @@ -0,0 +1,3 @@ +export * from './login'; +export * from './plan'; +export * from './common'; diff --git a/packages/cysync-core/src/services/inheritance/login/index.ts b/packages/cysync-core/src/services/inheritance/login/index.ts new file mode 100644 index 000000000..54d105d22 --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/login/index.ts @@ -0,0 +1,204 @@ +import { + resendResultSchema, + registerVerifyResultSchema, + initResultSchema, + validateResultSchema, + registerResultSchema, + InheritanceLoginEmailType, + verifyResultSchema, + refreshAccessTokenResultSchema, + updateNomineesResultSchema, + updateExecutorResultSchema, + ReminderPeriod, + updateReminderResultSchema, + NomineeType, + clearMeataDataResultSchema, +} from './schema'; + +import { + AuthTokenConfig, + makePostRequest, + runAndHandleServerErrors, +} from '../../utils'; +import { inheritanceBaseUrl, InheritanceUserType } from '../common'; + +export { + type InheritanceLoginInitResponse, + type InheritanceLoginResendResponse, + type InheritanceLoginRegisterVerifyResponse, + type InheritanceLoginConcern, + type InheritanceLoginValidateResponse, + type InheritanceLoginRegisterResponse, + type InheritanceLoginEmailType, + type InheritanceLoginVerifyResponse, + type InheritanceLoginRefreshAccessTokenResponse, + InheritanceLoginEmailTypeMap, + InheritanceLoginConcernMap, +} from './schema'; + +const baseUrl = `${inheritanceBaseUrl}/wallet-account`; + +export const InheritanceLoginAuthTypeMap = { + full: 'FULL', + seed: 'SEED', + wallet: 'WALLET', +} as const; + +export type InheritanceLoginAuthType = + (typeof InheritanceLoginAuthTypeMap)[keyof typeof InheritanceLoginAuthTypeMap]; + +const init = async (params: { + walletId: string; + loginType: InheritanceUserType; + authType: InheritanceLoginAuthType; +}) => + runAndHandleServerErrors(() => + makePostRequest(initResultSchema, `${baseUrl}/init`, params), + ); + +const validate = async (params: { + requestId: string; + seedPublicKey?: string; + walletPublicKey?: string; + seedSignature?: string; + walletSignature?: string; +}) => + runAndHandleServerErrors(() => + makePostRequest(validateResultSchema, `${baseUrl}/validate`, params), + ); + +const register = async (params: { + name: string; + requestId: string; + email: string; + alternateEmail: string; + walletName?: string; +}) => + runAndHandleServerErrors(() => + makePostRequest(registerResultSchema, `${baseUrl}/register/init`, params), + ); + +const resendOTP = async (params: { requestId: string }) => + runAndHandleServerErrors(() => + makePostRequest(resendResultSchema, `${baseUrl}/otp/resend`, params), + ); + +const registerVerify = async (params: { + requestId: string; + otp: string; + emailType: InheritanceLoginEmailType; +}) => + runAndHandleServerErrors(() => + makePostRequest(registerVerifyResultSchema, `${baseUrl}/register/verify`, { + requestId: params.requestId, + secret: params.otp, + emailType: params.emailType, + }), + ); + +const verify = async (params: { requestId: string; otp: string }) => + runAndHandleServerErrors(() => + makePostRequest(verifyResultSchema, `${baseUrl}/login`, { + requestId: params.requestId, + secret: params.otp, + }), + ); + +const refreshAccessToken = async (params: { refreshToken: string }) => + runAndHandleServerErrors(() => + makePostRequest( + refreshAccessTokenResultSchema, + `${baseUrl}/refresh-token`, + params, + ), + ); + +const updateNominees = async (params: { + nominee?: { name: string; email: string; alternateEmail?: string }; + requestId?: string; + secret?: string; + verify?: boolean; + nomineeType?: NomineeType; + authTokenConfig: AuthTokenConfig; +}) => + runAndHandleServerErrors(() => + makePostRequest( + updateNomineesResultSchema, + `${baseUrl}/info/nominee`, + { + nominee: params.nominee, + secret: params.secret, + verify: params.verify, + requestId: params.requestId, + nomineeType: params.nomineeType, + }, + params.authTokenConfig, + ), + ); + +const updateExecutor = async (params: { + name: string; + email: string; + alternateEmail: string; + nomineeEmail: string; + executorMessage?: string; + authTokenConfig: AuthTokenConfig; +}) => + runAndHandleServerErrors(() => + makePostRequest( + updateExecutorResultSchema, + `${baseUrl}/info/executor`, + { + name: params.name, + email: params.email, + alternateEmail: params.alternateEmail, + nominee: [params.nomineeEmail], + executorMessage: params.executorMessage, + }, + params.authTokenConfig, + ), + ); + +const updateReminder = async (params: { + frequency: ReminderPeriod; + authTokenConfig: AuthTokenConfig; +}) => + runAndHandleServerErrors(() => + makePostRequest( + updateReminderResultSchema, + `${baseUrl}/info/reminder`, + { frequency: params.frequency }, + params.authTokenConfig, + ), + ); + +const clearMetaData = async (params: { + resetExecutor?: boolean; + resetNominee?: boolean; + authTokenConfig: AuthTokenConfig; +}) => + runAndHandleServerErrors(() => + makePostRequest( + clearMeataDataResultSchema, + `${baseUrl}/info/clear`, + { + executor: params.resetExecutor === true, + nominee: params.resetNominee === true, + }, + params.authTokenConfig, + ), + ); + +export const inheritanceLoginService = { + init, + resendOTP, + verify, + registerVerify, + validate, + register, + refreshAccessToken, + updateNominees, + updateExecutor, + updateReminder, + clearMetaData, +}; diff --git a/packages/cysync-core/src/services/inheritance/login/schema.ts b/packages/cysync-core/src/services/inheritance/login/schema.ts new file mode 100644 index 000000000..fc250afe4 --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/login/schema.ts @@ -0,0 +1,108 @@ +import { z } from 'zod'; + +import { otpDetailSchema } from '../common'; + +export const InheritanceLoginConcernMap = { + LOGIN: 'WALLET_LOGIN_CHALLENGE', + REGISTER: 'WALLET_REGISTRATION_CHALLENGE', +} as const; + +export type InheritanceLoginConcern = + (typeof InheritanceLoginConcernMap)[keyof typeof InheritanceLoginConcernMap]; + +export const InheritanceLoginEmailTypeMap = { + PRIMARY: 'PRIMARY', + ALTERNATE: 'ALTERNATE', +} as const; + +export type InheritanceLoginEmailType = + (typeof InheritanceLoginEmailTypeMap)[keyof typeof InheritanceLoginEmailTypeMap]; + +const loginConcernSchema = z.enum([ + InheritanceLoginConcernMap.LOGIN, + InheritanceLoginConcernMap.REGISTER, +]); + +export const initResultSchema = z.object({ + requestId: z.string(), + challenge: z.string(), + concern: loginConcernSchema, +}); + +export const validateResultSchema = z.object({ + success: z.boolean(), + concern: loginConcernSchema, + otpDetails: z.array(otpDetailSchema).optional(), + wallet: z + .object({ + owner: z + .object({ + name: z.string().optional(), + email: z.string().optional(), + alternateEmail: z.string().optional(), + }) + .optional(), + }) + .optional() + .nullish(), +}); + +export const resendResultSchema = z.object({ + requestId: z.string(), + maskedEmail: z.string(), + retriesRemaining: z.number(), + otpExpiry: z.string().datetime(), +}); + +export const registerVerifyResultSchema = z.object({ + authToken: z.string().optional(), + refreshToken: z.string().optional(), +}); + +export const verifyResultSchema = z.object({ + authToken: z.string(), + refreshToken: z.string(), +}); + +export const registerResultSchema = z.object({ + otpDetails: z.array(otpDetailSchema), +}); + +export const refreshAccessTokenResultSchema = z.object({ + authToken: z.string(), +}); + +const genericSuccessResult = z.object({ + success: z.boolean().optional(), +}); + +export type ReminderPeriod = 'monthly' | 'quarterly' | 'half-yearly' | 'yearly'; + +export type NomineeType = 'PRIMARY' | 'ALTERNATE'; + +export const updateNomineesResultSchema = z.object({ + success: z.boolean().optional(), + otpDetails: otpDetailSchema + .extend({ requestId: z.string() }) + .array() + .optional(), +}); +export const updateExecutorResultSchema = genericSuccessResult; +export const updateReminderResultSchema = genericSuccessResult; +export const clearMeataDataResultSchema = genericSuccessResult; + +export type InheritanceLoginInitResponse = z.infer; +export type InheritanceLoginResendResponse = z.infer; +export type InheritanceLoginVerifyResponse = z.infer; +export type InheritanceLoginRegisterVerifyResponse = z.infer< + typeof registerVerifyResultSchema +>; +export type InheritanceLoginValidateResponse = z.infer< + typeof validateResultSchema +>; +export type InheritanceLoginRegisterResponse = z.infer< + typeof registerResultSchema +>; +export type InheritanceLoginRefreshAccessTokenResponse = z.infer< + typeof refreshAccessTokenResultSchema +>; diff --git a/packages/cysync-core/src/services/inheritance/plan/edit/index.ts b/packages/cysync-core/src/services/inheritance/plan/edit/index.ts new file mode 100644 index 000000000..ef22ddc05 --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/plan/edit/index.ts @@ -0,0 +1,30 @@ +import { editExecutorMessageResponseSchema } from './schema'; + +import { + AuthTokenConfig, + makePostRequest, + runAndHandleServerErrors, +} from '../../../utils'; +import { inheritanceBaseUrl } from '../../common'; + +export { type InheritanceEditExecutorMessageResponse } from './schema'; + +const baseUrl = `${inheritanceBaseUrl}/wallet-account`; + +const updateExecutorMessage = async (params: { + executorMessage: string; + sessionId: string; + authTokenConfig: AuthTokenConfig; +}) => + runAndHandleServerErrors(() => + makePostRequest( + editExecutorMessageResponseSchema, + `${baseUrl}/edit`, + params, + params.authTokenConfig, + ), + ); + +export const inheritanceEditPlansService = { + updateExecutorMessage, +}; diff --git a/packages/cysync-core/src/services/inheritance/plan/edit/schema.ts b/packages/cysync-core/src/services/inheritance/plan/edit/schema.ts new file mode 100644 index 000000000..e34ad105a --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/plan/edit/schema.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const editExecutorMessageResponseSchema = z.object({ + success: z.boolean(), +}); + +export type InheritanceEditExecutorMessageResponse = z.infer< + typeof editExecutorMessageResponseSchema +>; diff --git a/packages/cysync-core/src/services/inheritance/plan/index.ts b/packages/cysync-core/src/services/inheritance/plan/index.ts new file mode 100644 index 000000000..78c8c06b7 --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/plan/index.ts @@ -0,0 +1,112 @@ +import { inheritanceEditPlansService } from './edit'; +import { inheritanceRecoverPlansService } from './recover'; +import { + activateResultSchema, + applyCouponResultSchema, + checkCouponResultSchema, + createResultSchema, + getPlanResultSchema, +} from './schema'; +import { inheritanceSyncPlansService } from './sync'; + +import { + AuthTokenConfig, + makeGetRequest, + makePostRequest, + runAndHandleServerErrors, +} from '../../utils'; +import { inheritanceBaseUrl } from '../common'; + +export { + type InheritancePlanCreateResponse, + type InheritancePlanActivateResponse, + type InheritancePlanApplyCouponResponse, +} from './schema'; + +export * from './sync'; +export * from './edit'; + +const baseUrl = `${inheritanceBaseUrl}/wallet-account`; +const couponBaseUrl = `${inheritanceBaseUrl}/wallet-recovery`; + +const create = async (params: { + encryptedData: string; + sessionId: string; + authTokenConfig: AuthTokenConfig; +}) => + runAndHandleServerErrors(() => + makePostRequest( + createResultSchema, + `${baseUrl}/info/message`, + { + encryptedData: params.encryptedData, + sessionId: params.sessionId, + }, + params.authTokenConfig, + ), + ); + +const applyCoupon = async (params: { + coupon: string; + authTokenConfig: AuthTokenConfig; +}) => + runAndHandleServerErrors(() => + makePostRequest( + applyCouponResultSchema, + `${couponBaseUrl}/activate`, + { + coupon: params.coupon, + }, + params.authTokenConfig, + ), + ); + +const checkCoupon = async (params: { + coupon: string; + authTokenConfig: AuthTokenConfig; +}) => + runAndHandleServerErrors(() => + makePostRequest( + checkCouponResultSchema, + `${couponBaseUrl}/check-coupon`, + { + coupon: params.coupon, + }, + params.authTokenConfig, + ), + ); + +const activate = async (params: { + coupon: string; + authTokenConfig: AuthTokenConfig; +}) => + runAndHandleServerErrors(() => + makePostRequest( + activateResultSchema, + `${couponBaseUrl}/activate`, + { + coupon: params.coupon, + }, + params.authTokenConfig, + ), + ); + +const getPlan = async (params: { authTokenConfig: AuthTokenConfig }) => + runAndHandleServerErrors(() => + makeGetRequest( + getPlanResultSchema, + `${baseUrl}/list`, + params.authTokenConfig, + ), + ); + +export const inheritancePlanService = { + create, + checkCoupon, + applyCoupon, + activate, + sync: inheritanceSyncPlansService, + recover: inheritanceRecoverPlansService, + getPlan, + edit: inheritanceEditPlansService, +}; diff --git a/packages/cysync-core/src/services/inheritance/plan/recover/index.ts b/packages/cysync-core/src/services/inheritance/plan/recover/index.ts new file mode 100644 index 000000000..507a7fc2b --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/plan/recover/index.ts @@ -0,0 +1,39 @@ +import { recoverResultSchema } from './schema'; + +import { + AuthTokenConfig, + makePostRequest, + runAndHandleServerErrors, +} from '../../../utils'; +import { inheritanceBaseUrl } from '../../common'; + +export { type InheritanceRecoverPlanResponse } from './schema'; + +const baseUrl = `${inheritanceBaseUrl}/wallet-account`; + +const recover = async (params: { + sessionId: string; + authTokenConfig: AuthTokenConfig; + wallet?: boolean; + executor?: boolean; + nominee?: boolean; + message?: boolean; +}) => + runAndHandleServerErrors(() => + makePostRequest( + recoverResultSchema, + `${baseUrl}/recover`, + { + sessionId: params.sessionId, + wallet: params.wallet, + executor: params.executor, + nominee: params.nominee, + message: params.message, + }, + params.authTokenConfig, + ), + ); + +export const inheritanceRecoverPlansService = { + recover, +}; diff --git a/packages/cysync-core/src/services/inheritance/plan/recover/schema.ts b/packages/cysync-core/src/services/inheritance/plan/recover/schema.ts new file mode 100644 index 000000000..2e7fcd435 --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/plan/recover/schema.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const recoverResultSchema = z.object({ + executorMessage: z.string().nullish(), + encryptedMessage: z.string().optional(), +}); + +export type InheritanceRecoverPlanResponse = z.infer< + typeof recoverResultSchema +>; diff --git a/packages/cysync-core/src/services/inheritance/plan/schema.ts b/packages/cysync-core/src/services/inheritance/plan/schema.ts new file mode 100644 index 000000000..ab6665f7d --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/plan/schema.ts @@ -0,0 +1,81 @@ +import { z } from 'zod'; + +export const createResultSchema = z.object({}); +export const applyCouponResultSchema = z.object({}); +export const checkCouponResultSchema = z.object({ duration: z.string() }); +export const activateResultSchema = z.object({}); +export const getPlanResultSchema = z.object({ + emailConfig: z + .object({ + frequency: z.string().optional(), + }) + .optional(), + owner: z + .object({ + email: z.string().optional(), + alternateEmail: z.string().optional(), + name: z.string().optional(), + }) + .optional(), + executor: z + .object({ + nominee: z.array(z.string().optional()).optional(), + email: z.string().optional(), + alternateEmail: z.string().optional(), + name: z.string().optional(), + }) + .optional(), + _id: z.string(), + wallet: z.string(), + fullName: z.string().optional(), + metadata: z + .object({ + walletPublicKey: z.string().optional(), + seedPublicKey: z.string().optional(), + loginDisabled: z.boolean().optional(), + requirePasswordUpdate: z.boolean().optional(), + primaryEmailVerified: z.boolean().optional(), + alternateEmailVerified: z.boolean().optional(), + }) + .optional(), + nominee: z + .array( + z.object({ + email: z.string().optional(), + alternateEmail: z.string().optional(), + name: z.string().optional(), + }), + ) + .optional(), + subscription: z + .array( + z.object({ + order: z + .object({ + _id: z.string(), + planType: z.string().optional(), + activationDate: z.string().optional(), + expiryDate: z.string().optional(), + }) + .optional(), + walletPlan: z.string(), + activationDate: z.string(), + _id: z.string(), + }), + ) + .optional(), + createdAt: z.string(), + updatedAt: z.string(), + __v: z.number(), +}); + +export type InheritancePlanCreateResponse = z.infer; +export type InheritancePlanApplyCouponResponse = z.infer< + typeof applyCouponResultSchema +>; +export type InheritancePlanActivateResponse = z.infer< + typeof activateResultSchema +>; +export type InheritancePlanGetPlanResponse = z.infer< + typeof getPlanResultSchema +>; diff --git a/packages/cysync-core/src/services/inheritance/plan/sync/index.ts b/packages/cysync-core/src/services/inheritance/plan/sync/index.ts new file mode 100644 index 000000000..4f4bed2c0 --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/plan/sync/index.ts @@ -0,0 +1,29 @@ +import { initResultSchema, verifyResultSchema } from './schema'; + +import { makePostRequest, runAndHandleServerErrors } from '../../../utils'; +import { inheritanceBaseUrl } from '../../common'; + +export { + type InheritanceSyncPlansVerifyResponse, + type InheritanceSyncPlansInitResponse, +} from './schema'; + +const baseUrl = `${inheritanceBaseUrl}/wallet-account/sync`; + +const init = async (params: { email: string }) => + runAndHandleServerErrors(() => + makePostRequest(initResultSchema, `${baseUrl}/init`, params), + ); + +const verify = async (params: { requestId: string; otp: string }) => + runAndHandleServerErrors(() => + makePostRequest(verifyResultSchema, `${baseUrl}/verify`, { + requestId: params.requestId, + secret: params.otp, + }), + ); + +export const inheritanceSyncPlansService = { + init, + verify, +}; diff --git a/packages/cysync-core/src/services/inheritance/plan/sync/schema.ts b/packages/cysync-core/src/services/inheritance/plan/sync/schema.ts new file mode 100644 index 000000000..8c4faa591 --- /dev/null +++ b/packages/cysync-core/src/services/inheritance/plan/sync/schema.ts @@ -0,0 +1,40 @@ +import { z } from 'zod'; + +import { otpDetailSchema } from '../../common'; + +export const initResultSchema = z.object({ + requestId: z.string(), + otpDetails: z.array(otpDetailSchema).optional(), +}); + +export const verifyResultSchema = z.object({ + wallets: z + .array( + z.object({ + _id: z.string(), + wallet: z.string(), + fullName: z.string().optional(), + role: z.string().optional(), + subscription: z + .array( + z.object({ + order: z + .object({ + _id: z.string(), + planType: z.string().optional(), + activationDate: z.string().optional(), + expiryDate: z.string().optional(), + }) + .optional(), + }), + ) + .optional(), + }), + ) + .optional(), +}); + +export type InheritanceSyncPlansInitResponse = z.infer; +export type InheritanceSyncPlansVerifyResponse = z.infer< + typeof verifyResultSchema +>; diff --git a/packages/cysync-core/src/services/utils/index.ts b/packages/cysync-core/src/services/utils/index.ts new file mode 100644 index 000000000..3551c38ee --- /dev/null +++ b/packages/cysync-core/src/services/utils/index.ts @@ -0,0 +1,215 @@ +import { ServerError, ServerErrorType } from '@cypherock/cysync-core-constants'; +import axios, { + AxiosError, + AxiosRequestConfig, + AxiosResponse, + HttpStatusCode, +} from 'axios'; + +import { createServerErrorFromError } from '~/utils'; + +import { inheritanceBaseUrl } from '../inheritance'; +import { refreshAccessTokenResultSchema } from '../inheritance/login/schema'; + +export const serverErrorCodeMap: Record = { + 1001: ServerErrorType.OTP_VERIFICATION_FAILED, + 1003: ServerErrorType.LOGIN_FAILED, + 1004: ServerErrorType.SIGNATURE_VERIFICATION_FAILED, + 1005: ServerErrorType.INVALID_REQUEST, + 1006: ServerErrorType.UNAUTHORIZED_ACCESS, + 1007: ServerErrorType.RESOURCE_NOT_FOUND, + 1008: ServerErrorType.INTERNAL_SERVER_ERROR, + 1009: ServerErrorType.REQUEST_TIMEOUT, + 1010: ServerErrorType.OTP_EXPIRED, + 1011: ServerErrorType.PAYLOAD_VALIDATION_ERROR, + 1012: ServerErrorType.MAX_RETRIES_EXCEEDED, + 1013: ServerErrorType.ACCOUNT_LOCKED, + 1014: ServerErrorType.SERVICE_UNAVAILABLE, + 1015: ServerErrorType.REQUEST_CONFLICT, + 1016: ServerErrorType.VALIDATION_ERROR, + 1017: ServerErrorType.MISSING_WALLET, + 1018: ServerErrorType.MISSING_USER, + 1019: ServerErrorType.MISSING_NOMINEE, + 1020: ServerErrorType.MISSING_EXECUTOR, + 1021: ServerErrorType.MISSING_SESSION, + 1022: ServerErrorType.INVALID_COUPON, + 1023: ServerErrorType.INVALID_SESSION, + 1024: ServerErrorType.INVALID_DEVICE, + 1025: ServerErrorType.INVALID_PRIVATE_KEY, + 1026: ServerErrorType.ACTIVE_PLAN_FOUND, +}; + +export type Only = { + [P in keyof T]: T[P]; +} & { + [P in keyof U]?: never; +}; + +export type Either = Only | Only; + +export interface ServerErrorResponse { + error: ServerError; +} + +export interface ServerResultResponse { + result: T; +} + +export type ServerResponseWithError = Either< + ServerErrorResponse, + ServerResultResponse +>; + +export async function runAndHandleServerErrors( + callback: () => Promise, +): Promise> { + try { + const result = await callback(); + return { + result, + }; + } catch (error) { + if ((error as any)?.isAxiosError) { + const e = error as AxiosError; + + if ((e.response?.data as any).code) { + const { code } = e.response?.data as any; + const errorType = + serverErrorCodeMap[code] ?? ServerErrorType.UNKNOWN_ERROR; + const serverError = + createServerErrorFromError(e, errorType) ?? + new ServerError(ServerErrorType.UNKNOWN_ERROR); + + return { + error: serverError as ServerError, + }; + } + } + + throw error; + } +} + +export interface RefreshTokenConfig { + refreshToken: string; + updateAuthToken: (newAuthToken: string) => void; +} + +export interface AuthTokenConfig { + accessToken: string; + refreshTokenConfig?: RefreshTokenConfig; +} + +export async function makePostRequest( + schema: Zod.Schema, + url: string, + data: any, + authTokenConfig?: AuthTokenConfig, + axiosConfig?: AxiosRequestConfig, +) { + return autoRefreshTokenRequest( + (accessToken: string) => + makeRequest( + { + schema, + accessToken, + config: axiosConfig, + data, + }, + ({ config, data: postData }) => axios.post(url, postData, config), + ), + authTokenConfig, + axiosConfig, + ); +} + +// TODO: Maybe localise this function for inheritance scope? and make refresh-token API injectable? +export async function autoRefreshTokenRequest( + requestFunction: (accessToken: string) => Promise, + authTokenConfig?: AuthTokenConfig, + axiosConfig?: AxiosRequestConfig, +) { + let result; + try { + result = await requestFunction(authTokenConfig?.accessToken ?? ''); + } catch (error) { + const { refreshTokenConfig } = authTokenConfig ?? {}; + if (!refreshTokenConfig || !(error as any).isAxiosError) throw error; + + const axiosError = error as AxiosError; + if (axiosError.response?.status !== HttpStatusCode.Unauthorized) + throw error; + + let newAuthToken = ''; + // won't recurse because we are not providing authTokenConfig in this call + const refreshTokenResponse = await makePostRequest( + refreshAccessTokenResultSchema, + `${inheritanceBaseUrl}/wallet-account/refresh-token`, + { + refreshToken: refreshTokenConfig.refreshToken, + }, + undefined, + axiosConfig, + ); + + newAuthToken = refreshTokenResponse.authToken; + if (!newAuthToken) throw error; + + refreshTokenConfig.updateAuthToken(newAuthToken); + result = await requestFunction(newAuthToken ?? ''); + } + return result; +} + +export async function makeGetRequest( + schema: Zod.Schema, + url: string, + authTokenConfig?: AuthTokenConfig, + axiosConfig?: AxiosRequestConfig, +) { + return autoRefreshTokenRequest( + (accessToken: string) => + makeRequest( + { + schema, + accessToken, + config: axiosConfig, + }, + ({ config }) => axios.get(url, config), + ), + authTokenConfig, + axiosConfig, + ); +} + +export type RequestFunction = (params: { + config?: AxiosRequestConfig; + data?: any; +}) => Promise; + +async function makeRequest( + params: { + schema: Zod.Schema; + accessToken?: string; + config?: AxiosRequestConfig; + data?: any; + }, + requestFunction: RequestFunction, +) { + const { schema, accessToken, data } = params; + let { config } = params; + + if (accessToken) { + config = { + ...(config ?? {}), + headers: { + ...config?.headers, + Authorization: `Bearer ${accessToken}`, + }, + }; + } + + const response = await requestFunction({ config, data }); + const result = schema.parse(response.data); + return result; +} diff --git a/packages/cysync-core/src/store/dialog/index.ts b/packages/cysync-core/src/store/dialog/index.ts index 15fa7c327..5b569373a 100644 --- a/packages/cysync-core/src/store/dialog/index.ts +++ b/packages/cysync-core/src/store/dialog/index.ts @@ -86,6 +86,36 @@ const initialState: IDialogState = { betaNotificationDialog: { isOpen: false, }, + inheritanceSyncPlans: { + isOpen: false, + }, + inheritancePlanLogin: { + isOpen: false, + }, + inheritanceSilverPlanPurchase: { + isOpen: false, + }, + inheritanceGoldPlanPurchase: { + isOpen: false, + }, + inheritanceEditExecutorMessage: { + isOpen: false, + }, + inheritanceEditReminderTime: { + isOpen: false, + }, + inheritanceEditUserDetails: { + isOpen: false, + }, + inheritancePinRecovery: { + isOpen: false, + }, + inheritanceEditEncryptedMessage: { + isOpen: false, + }, + inheritanceEstateRecovery: { + isOpen: false, + }, }; export const dialogSlice = createSlice({ diff --git a/packages/cysync-core/src/store/dialog/types.ts b/packages/cysync-core/src/store/dialog/types.ts index 94a784fbe..6e3888200 100644 --- a/packages/cysync-core/src/store/dialog/types.ts +++ b/packages/cysync-core/src/store/dialog/types.ts @@ -3,6 +3,12 @@ import { AddTokenDialogProps } from '~/dialogs/AddToken'; import { DeleteAccountDialogProps } from '~/dialogs/DeleteAccountDialog'; import { ErrorDialogProps } from '~/dialogs/ErrorDialog'; import { IHistoryDialogProps } from '~/dialogs/HistoryDialog'; +import { + InheritanceEstateRecoveryDialogProps, + InheritancePinRecoveryDialogProps, + InheritancePlanLoginDialogProps, +} from '~/dialogs/Inheritance'; +import { InheritanceEditExecutorMessageDialogProps } from '~/dialogs/Inheritance/EditExecutorMessage/context'; import { SendDialogProps } from '~/dialogs/Send'; export interface IDialogState { @@ -132,6 +138,56 @@ export interface IDialogState { isOpen: boolean; data?: undefined; }; + + inheritanceSyncPlans: { + isOpen: boolean; + data?: undefined; + }; + + inheritancePlanLogin: { + isOpen: boolean; + data?: InheritancePlanLoginDialogProps; + }; + + inheritanceSilverPlanPurchase: { + isOpen: boolean; + data?: undefined; + }; + + inheritanceGoldPlanPurchase: { + isOpen: boolean; + data?: undefined; + }; + + inheritanceEditExecutorMessage: { + isOpen: boolean; + data?: InheritanceEditExecutorMessageDialogProps; + }; + + inheritanceEditReminderTime: { + isOpen: boolean; + data?: undefined; + }; + + inheritanceEditUserDetails: { + isOpen: boolean; + data?: undefined; + }; + + inheritancePinRecovery: { + isOpen: boolean; + data?: InheritancePinRecoveryDialogProps; + }; + + inheritanceEditEncryptedMessage: { + isOpen: boolean; + data?: undefined; + }; + + inheritanceEstateRecovery: { + isOpen: boolean; + data?: InheritanceEstateRecoveryDialogProps; + }; } export const GuidedFlowMap = { diff --git a/packages/cysync-core/src/store/index.ts b/packages/cysync-core/src/store/index.ts index 210d7cd9a..35782548b 100644 --- a/packages/cysync-core/src/store/index.ts +++ b/packages/cysync-core/src/store/index.ts @@ -12,3 +12,4 @@ export * from './accountSync'; export * from './snackBar'; export * from './notification'; export * from './network'; +export * from './inheritance'; diff --git a/packages/cysync-core/src/store/inheritance/index.ts b/packages/cysync-core/src/store/inheritance/index.ts new file mode 100644 index 000000000..9869c4358 --- /dev/null +++ b/packages/cysync-core/src/store/inheritance/index.ts @@ -0,0 +1,91 @@ +// Issue with reduxjs/toolkit: https://github.com/reduxjs/redux-toolkit/issues/1806 +// eslint-disable-next-line import/no-extraneous-dependencies +import 'immer'; +import { IInheritancePlan } from '@cypherock/db-interfaces'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +import { + IInheritancePlanDetails, + IInheritanceState, + IWalletAuthTokens, +} from './types'; + +import type { RootState } from '../store'; + +export * from './types'; + +const initialState: IInheritanceState = { + isLoaded: false, + walletAuthTokens: {}, + seedAuthTokens: {}, + inheritancePlans: [], + inheritancePlanDetails: {}, +} as IInheritanceState; + +export const inheritanceSlice = createSlice({ + name: 'inheritance', + initialState, + reducers: { + updateWalletAuthTokens: ( + state, + action: PayloadAction<{ + walletId: string; + authTokens: IWalletAuthTokens; + }>, + ) => { + state.walletAuthTokens[action.payload.walletId] = + action.payload.authTokens; + return state; + }, + updateSeedAuthTokens: ( + state, + action: PayloadAction<{ + walletId: string; + authTokens: IWalletAuthTokens; + }>, + ) => { + state.seedAuthTokens[action.payload.walletId] = action.payload.authTokens; + return state; + }, + setInheritancePlans: ( + state, + payload: PayloadAction, + ) => { + state.inheritancePlans = payload.payload; + state.isLoaded = true; + }, + updateInheritancePlanDetails: ( + state, + payload: PayloadAction<{ + walletId: string; + planDetails: IInheritancePlanDetails; + }>, + ) => { + state.inheritancePlanDetails[payload.payload.walletId] = + payload.payload.planDetails; + return state; + }, + clearInheritancePlanDetails: state => { + state.inheritancePlanDetails = {}; + return state; + }, + }, +}); + +export const { + updateWalletAuthTokens, + updateSeedAuthTokens, + setInheritancePlans, + updateInheritancePlanDetails, + clearInheritancePlanDetails, +} = inheritanceSlice.actions; + +export const selectInheritanceWalletAuthTokens = (state: RootState) => + state.inheritance.walletAuthTokens; +export const selectInheritanceSeedAuthTokens = (state: RootState) => + state.inheritance.seedAuthTokens; + +export const selectInheritancePlans = (state: RootState) => + state.inheritance.inheritancePlans; + +export default inheritanceSlice.reducer; diff --git a/packages/cysync-core/src/store/inheritance/types.ts b/packages/cysync-core/src/store/inheritance/types.ts new file mode 100644 index 000000000..49b0e1add --- /dev/null +++ b/packages/cysync-core/src/store/inheritance/types.ts @@ -0,0 +1,53 @@ +import { IInheritancePlan } from '@cypherock/db-interfaces'; +import { ReminderPeriod } from '~/services/inheritance/login/schema'; + +export interface IWalletAuthTokens { + accessToken: string; + refreshToken: string; +} + +export const reminderPeriodStringToNumMap: Record = { + monthly: 1, + quarterly: 3, + 'half-yearly': 6, + yearly: 12, + '': 0, +} as const; + +export const reminderPeriodNumToStringMap: Record = { + 1: 'monthly', + 3: 'quarterly', + 6: 'half-yearly', + 12: 'yearly', +} as const; + +export interface IInheritancePlanDetails { + walletId: string; + name: string; + activationDate: number; + expiryDate: number; + owner: { + name: string; + email: string; + alternateEmail: string; + }; + nominee: { + name: string; + email: string; + alternateEmail: string; + }[]; + executor?: { + name: string; + email: string; + alternateEmail: string; + }; + reminderPeriod: number; +} + +export interface IInheritanceState { + walletAuthTokens: Record; + seedAuthTokens: Record; + inheritancePlans: IInheritancePlan[]; + inheritancePlanDetails: Record; + isLoaded: boolean; +} diff --git a/packages/cysync-core/src/store/store.ts b/packages/cysync-core/src/store/store.ts index 497ae4fe9..8e56a8475 100644 --- a/packages/cysync-core/src/store/store.ts +++ b/packages/cysync-core/src/store/store.ts @@ -16,6 +16,7 @@ import accountSyncReducer, { IAccountSyncState } from './accountSync'; import deviceReducer, { IDeviceState } from './device'; import dialogReducer, { IDialogState } from './dialog'; import discreetModeReducer, { IDiscreetModeState } from './discreetMode'; +import inheritanceReducer, { IInheritanceState } from './inheritance'; import langReducers, { ILangState } from './lang'; import networkReducer, { INetworkState } from './network'; import notificationReducer, { INotificationState } from './notification'; @@ -39,6 +40,7 @@ export interface RootState { snackBar: ISnackBarState; notification: INotificationState; network: INetworkState; + inheritance: IInheritanceState; } export const store = configureStore({ @@ -56,6 +58,7 @@ export const store = configureStore({ snackBar: snackBarReducer, notification: notificationReducer, network: networkReducer, + inheritance: inheritanceReducer, }, }); diff --git a/packages/cysync-core/src/utils/error/index.ts b/packages/cysync-core/src/utils/error/index.ts index 6050816d0..c61721f7e 100644 --- a/packages/cysync-core/src/utils/error/index.ts +++ b/packages/cysync-core/src/utils/error/index.ts @@ -30,7 +30,10 @@ export * from './types'; /** * Assuming we are using axios for server calls */ -const identifyServerErrors = (error: any) => { +export const createServerErrorFromError = ( + error: any, + type?: ServerErrorType, +) => { if (error?.isAxiosError) { if (error.response && error.response.data?.coinErrorCode) { return new ServerCoinError({ @@ -45,7 +48,7 @@ const identifyServerErrors = (error: any) => { }); } if (error.response) { - return new ServerError(ServerErrorType.UNKNOWN_ERROR, undefined, { + return new ServerError(type ?? ServerErrorType.UNKNOWN_ERROR, undefined, { advanceText: error?.response?.data?.cysyncError, responseBody: error?.response?.data, url: error?.request?.url, @@ -67,7 +70,7 @@ export const getParsedError = (params: { }): IParsedError => { const { error, lang, retries } = params; - const errorToParse = identifyServerErrors(error) ?? error; + const errorToParse = createServerErrorFromError(error) ?? error; let heading = params.defaultMsg ?? lang.strings.errors.default; let subtext: string | undefined; diff --git a/packages/cysync-core/src/utils/index.ts b/packages/cysync-core/src/utils/index.ts index b6ad0a132..5d4d3ee29 100644 --- a/packages/cysync-core/src/utils/index.ts +++ b/packages/cysync-core/src/utils/index.ts @@ -19,3 +19,4 @@ export * from './uuid'; export * from './validator'; export * from './walletConnect'; export * from './file'; +export * from './inheritance'; diff --git a/packages/cysync-core/src/utils/inheritance.ts b/packages/cysync-core/src/utils/inheritance.ts new file mode 100644 index 000000000..be9a97395 --- /dev/null +++ b/packages/cysync-core/src/utils/inheritance.ts @@ -0,0 +1,3 @@ +import { InheritanceSupport } from '@cypherock/app-support-inheritance'; + +export const inheritanceSupport = new InheritanceSupport(); diff --git a/packages/cysync-core/src/utils/logger.ts b/packages/cysync-core/src/utils/logger.ts index 71484a157..738c38921 100644 --- a/packages/cysync-core/src/utils/logger.ts +++ b/packages/cysync-core/src/utils/logger.ts @@ -1,3 +1,4 @@ +import { updateLogger as updateLoggerAppSupportInheritance } from '@cypherock/app-support-inheritance'; import { updateLogger as updateLoggerCoinSupportBtc } from '@cypherock/coin-support-btc'; import { updateLogger as updateLoggerCoinSupportEvm } from '@cypherock/coin-support-evm'; import { updateLogger as updateLoggerCoinSupportNear } from '@cypherock/coin-support-near'; @@ -39,6 +40,7 @@ export const updateLogger = (createLogger: LogCreator) => { updateLoggerCoinSupportEvm(createLogger); updateLoggerCoinSupportSolana(createLogger); updateLoggerCoinSupportNear(createLogger); + updateLoggerAppSupportInheritance(createLogger); }; export default logger; diff --git a/packages/cysync-core/src/utils/validator.ts b/packages/cysync-core/src/utils/validator.ts index abfca9ad5..8f10065a7 100644 --- a/packages/cysync-core/src/utils/validator.ts +++ b/packages/cysync-core/src/utils/validator.ts @@ -1,16 +1,26 @@ -import { z } from 'zod'; +import { z, ZodSchema } from 'zod'; import { ILangState } from '..'; -export const validateEmail = (email: string, lang: ILangState) => { - const EmailSchema = z +export const getEmailValidationSchema = ( + lang: ILangState, + isRequired = true, +) => { + let schema: ZodSchema = z .string() - .min(1, { message: lang.strings.validation.generic.required }) + .min(1, { + message: lang.strings.validation.generic.required, + }) .email({ message: lang.strings.validation.email.invalid }); - return EmailSchema.safeParse(email); + if (!isRequired) schema = z.union([z.literal(''), schema.optional()]); + + return schema; }; +export const validateEmail = (email: string, lang: ILangState) => + getEmailValidationSchema(lang).safeParse(email); + const passwordPrefixed = (prefix: string, lang: ILangState) => z .string() @@ -56,3 +66,13 @@ export const validatePassword = ( }); return PasswordSchema.safeParse(passwordObj); }; + +export const validateInputLanguage = (input: string, lang: ILangState) => { + const asciiCharsRegex = /^[\x20-\x7E\n\r]*$/; + + const englishAsciiSchema = z + .string() + .regex(asciiCharsRegex, lang.strings.validation.generic.englishOnly); + + return englishAsciiSchema.safeParse(input); +}; diff --git a/packages/database/src/database.ts b/packages/database/src/database.ts index 135d4fc33..8791352a2 100644 --- a/packages/database/src/database.ts +++ b/packages/database/src/database.ts @@ -21,12 +21,15 @@ import { ITransactionNotificationReadRepository, IMigrationRepository, IMigration, + IInheritancePlan, + IInheritancePlanRepository, } from '@cypherock/db-interfaces'; import { EncryptedDB } from './encryptedDb'; import { Account, Device, + InheritancePlan, Migration, PriceHistory, PriceInfo, @@ -62,6 +65,8 @@ export class Database implements IDatabase { migration: IMigrationRepository; + inheritancePlan: IInheritancePlanRepository; + constructor(params: { database: EncryptedDB; device: IDeviceRepository; @@ -73,6 +78,7 @@ export class Database implements IDatabase { transactionNotificationClick: ITransactionNotificationClickRepository; transactionNotificationRead: ITransactionNotificationReadRepository; migration: IMigrationRepository; + inheritancePlan: IInheritancePlanRepository; }) { this.database = params.database; @@ -85,6 +91,7 @@ export class Database implements IDatabase { this.transactionNotificationClick = params.transactionNotificationClick; this.transactionNotificationRead = params.transactionNotificationRead; this.migration = params.migration; + this.inheritancePlan = params.inheritancePlan; } public static async create(dirPath: string) { @@ -137,6 +144,11 @@ export class Database implements IDatabase { Migration.name, Migration.schema, ); + const inheritancePlan = await Repository.create( + database, + InheritancePlan.name, + InheritancePlan.schema, + ); return new Database({ database, @@ -149,6 +161,7 @@ export class Database implements IDatabase { transactionNotificationClick, transactionNotificationRead, migration, + inheritancePlan, }); } diff --git a/packages/database/src/entity/index.ts b/packages/database/src/entity/index.ts index 89db5c3d9..7c53aba06 100644 --- a/packages/database/src/entity/index.ts +++ b/packages/database/src/entity/index.ts @@ -8,3 +8,4 @@ export * from './types'; export * from './transactionNotificationClick'; export * from './transactionNotificationRead'; export * from './migration'; +export * from './inheritancePlan'; diff --git a/packages/database/src/entity/inheritancePlan.ts b/packages/database/src/entity/inheritancePlan.ts new file mode 100644 index 000000000..5603b8f45 --- /dev/null +++ b/packages/database/src/entity/inheritancePlan.ts @@ -0,0 +1,17 @@ +import { IInheritancePlan } from '@cypherock/db-interfaces'; + +import { BaseFields, ITableDetails } from './types'; + +export const InheritancePlan: ITableDetails< + Omit +> = { + name: 'inheritancePlan', + schema: { + walletId: { type: 'string' }, + walletName: { type: 'string' }, + type: { type: 'string' }, + isNominee: { type: 'boolean' }, + purchasedAt: { type: 'number', isOptional: true }, + expireAt: { type: 'number', isOptional: true }, + }, +}; diff --git a/packages/db-interfaces/src/entities/index.ts b/packages/db-interfaces/src/entities/index.ts index 9d3c87509..1e0c80d7c 100644 --- a/packages/db-interfaces/src/entities/index.ts +++ b/packages/db-interfaces/src/entities/index.ts @@ -8,3 +8,4 @@ export * from './wallet'; export * from './transactionNotificationRead'; export * from './transactionNotificationClick'; export * from './migrations'; +export * from './inheritancePlan'; diff --git a/packages/db-interfaces/src/entities/inheritancePlan.ts b/packages/db-interfaces/src/entities/inheritancePlan.ts new file mode 100644 index 000000000..1eaeb0e38 --- /dev/null +++ b/packages/db-interfaces/src/entities/inheritancePlan.ts @@ -0,0 +1,20 @@ +import type { IEntity, IRepository } from './base'; + +export const InheritancePlanTypeMap = { + gold: 'gold', + silver: 'silver', +} as const; + +export type InheritancePlanType = + (typeof InheritancePlanTypeMap)[keyof typeof InheritancePlanTypeMap]; + +export interface IInheritancePlan extends IEntity { + walletId: string; + walletName: string; + type: InheritancePlanType; + isNominee: boolean; + purchasedAt?: number; + expireAt?: number; +} + +export type IInheritancePlanRepository = IRepository; diff --git a/packages/db-interfaces/src/index.ts b/packages/db-interfaces/src/index.ts index 0d94db808..34b16562a 100644 --- a/packages/db-interfaces/src/index.ts +++ b/packages/db-interfaces/src/index.ts @@ -11,6 +11,7 @@ import type { ITransactionNotificationClickRepository, ITransactionNotificationReadRepository, IMigrationRepository, + IInheritancePlanRepository, } from './entities'; export interface IDatabase { @@ -26,6 +27,7 @@ export interface IDatabase { priceHistory: IPriceHistoryRepository; priceInfo: IPriceInfoRepository; migration: IMigrationRepository; + inheritancePlan: IInheritancePlanRepository; changeEncryptionKey(encryptionKey?: string): Promise; createOrFetchRepository( name: string, diff --git a/packages/desktop-ui/src/Router.tsx b/packages/desktop-ui/src/Router.tsx index a09609701..80735f6b4 100644 --- a/packages/desktop-ui/src/Router.tsx +++ b/packages/desktop-ui/src/Router.tsx @@ -23,6 +23,9 @@ import { AppUpdate, AssetPage, AccountPage, + InheritanceHome, + InheritanceChoosePlan, + InheritancePlanDetails, } from '@cypherock/cysync-core'; import React, { memo, ReactNode } from 'react'; import { HashRouter as Router, Route, Routes } from 'react-router-dom'; @@ -48,6 +51,9 @@ const components: Record = { account: , history: , settings: , + inheritance: , + 'inheritance-choose-plan': , + 'inheritance-plan-details': , }; export type InternalRoute = Record; diff --git a/packages/ui/.storybook/main.ts b/packages/ui/.storybook/main.ts index bce3d2746..422a5d126 100644 --- a/packages/ui/.storybook/main.ts +++ b/packages/ui/.storybook/main.ts @@ -15,7 +15,6 @@ const config: StorybookConfig = { getAbsolutePath('@storybook/addon-links'), getAbsolutePath('@storybook/addon-docs'), getAbsolutePath('@storybook/addon-essentials'), - getAbsolutePath('@storybook/addon-onboarding'), getAbsolutePath('@storybook/addon-interactions'), getAbsolutePath('@storybook/addon-styling'), ], diff --git a/packages/ui/icons/account-icon.svg b/packages/ui/icons/account-icon.svg new file mode 100644 index 000000000..10958b670 --- /dev/null +++ b/packages/ui/icons/account-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/icons/alertIcon.svg b/packages/ui/icons/alertIcon.svg new file mode 100644 index 000000000..a0b4e430f --- /dev/null +++ b/packages/ui/icons/alertIcon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/ui/icons/arrow-back-golden-icon.svg b/packages/ui/icons/arrow-back-golden-icon.svg new file mode 100644 index 000000000..f927c6a22 --- /dev/null +++ b/packages/ui/icons/arrow-back-golden-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/ui/icons/check-mark-circle.svg b/packages/ui/icons/check-mark-circle.svg new file mode 100644 index 000000000..022afe1a3 --- /dev/null +++ b/packages/ui/icons/check-mark-circle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/ui/icons/clock-icon.svg b/packages/ui/icons/clock-icon.svg new file mode 100644 index 000000000..2a8d712e2 --- /dev/null +++ b/packages/ui/icons/clock-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/ui/icons/cross-icon.svg b/packages/ui/icons/cross-icon.svg new file mode 100644 index 000000000..a74f2194b --- /dev/null +++ b/packages/ui/icons/cross-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/icons/cypherock-cover-icon.svg b/packages/ui/icons/cypherock-cover-icon.svg new file mode 100644 index 000000000..343761f22 --- /dev/null +++ b/packages/ui/icons/cypherock-cover-icon.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/icons/delete-icon.svg b/packages/ui/icons/delete-icon.svg new file mode 100644 index 000000000..90ad35f11 --- /dev/null +++ b/packages/ui/icons/delete-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/ui/icons/email-icon-small.svg b/packages/ui/icons/email-icon-small.svg new file mode 100644 index 000000000..5f1971e79 --- /dev/null +++ b/packages/ui/icons/email-icon-small.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/icons/encrypted-message-icon.svg b/packages/ui/icons/encrypted-message-icon.svg new file mode 100644 index 000000000..2de8e6ef6 --- /dev/null +++ b/packages/ui/icons/encrypted-message-icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/ui/icons/hourglass-icon.svg b/packages/ui/icons/hourglass-icon.svg new file mode 100644 index 000000000..53ecbbbe8 --- /dev/null +++ b/packages/ui/icons/hourglass-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/ui/icons/message-icon.svg b/packages/ui/icons/message-icon.svg new file mode 100644 index 000000000..393ba4c22 --- /dev/null +++ b/packages/ui/icons/message-icon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/packages/ui/icons/mim-hover-wallet-icon.svg b/packages/ui/icons/mim-hover-wallet-icon.svg new file mode 100644 index 000000000..2e71e7126 --- /dev/null +++ b/packages/ui/icons/mim-hover-wallet-icon.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/icons/open-external-link-icon.svg b/packages/ui/icons/open-external-link-icon.svg new file mode 100644 index 000000000..5082a4875 --- /dev/null +++ b/packages/ui/icons/open-external-link-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/ui/icons/plus-icon.svg b/packages/ui/icons/plus-icon.svg new file mode 100644 index 000000000..68be19b34 --- /dev/null +++ b/packages/ui/icons/plus-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/ui/icons/shopping-cart.svg b/packages/ui/icons/shopping-cart.svg new file mode 100644 index 000000000..20262fcc6 --- /dev/null +++ b/packages/ui/icons/shopping-cart.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/icons/user-icon.svg b/packages/ui/icons/user-icon.svg new file mode 100644 index 000000000..00997a301 --- /dev/null +++ b/packages/ui/icons/user-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/icons/wallet-icon-rounded.svg b/packages/ui/icons/wallet-icon-rounded.svg new file mode 100644 index 000000000..2f4cad759 --- /dev/null +++ b/packages/ui/icons/wallet-icon-rounded.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/package.json b/packages/ui/package.json index cd15d6892..f4dbabb09 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -16,8 +16,9 @@ "build:esm": "tsc -p tsconfig.json && copyfiles -u 1 src/**/*.{png,jpg,svg,jpeg,gif,webm} dist/esm", "build:cjs": "tsc -p tsconfig_cjs.json && copyfiles -u 1 src/**/*.{png,jpg,svg,jpeg,gif,webm} dist/cjs", "build:icons": "svgr icons --out-dir ./src/assets/icons/generated --no-svgo", - "prebuild": "pnpm build:icons", + "prebuild": "pnpm clean:icons && pnpm build:icons", "clean": "rimraf dist", + "clean:icons": "rimraf ./src/assets/icons/generated", "test": "jest", "pre-commit": "lint-staged", "storybook": "storybook dev -p 6006", @@ -33,7 +34,6 @@ "@storybook/addon-essentials": "7.2.0", "@storybook/addon-interactions": "7.2.0", "@storybook/addon-links": "7.2.0", - "@storybook/addon-onboarding": "1.0.8", "@storybook/addon-styling": "^1.3.5", "@storybook/blocks": "7.2.0", "@storybook/jest": "^0.1.0", @@ -74,12 +74,14 @@ "chart.js": "^4.3.2", "date-fns": "^2.30.0", "focus-trap-react": "^10.2.3", - "marked-react": "^2.0.0", + "marked": "^13.0.2", "rc-tooltip": "^6.1.2", "react": "^18.2.0", + "react-circular-progressbar": "^2.1.0", "react-dom": "^18.2.0", "react-json-view": "^1.21.3", "react-multi-date-picker": "^4.1.2", + "react-otp-input": "^3.1.1", "react-tsparticles": "^2.10.1", "react-virtualized": "^9.22.5", "styled-components": "6.0.0-rc.1", diff --git a/packages/ui/src/assets/icons/MimDefaultWallet.tsx b/packages/ui/src/assets/icons/MimDefaultWallet.tsx new file mode 100644 index 000000000..86143fd4c --- /dev/null +++ b/packages/ui/src/assets/icons/MimDefaultWallet.tsx @@ -0,0 +1,59 @@ +import React from 'react'; + +import { SvgStyle as Svg, SvgProps } from './Svg'; + +export const MimDefaultWalletIcon: React.FC = ({ + stroke, + fill, + ...props +}) => ( + + + + + + + + + + + + + +); diff --git a/packages/ui/src/assets/icons/index.ts b/packages/ui/src/assets/icons/index.ts index e104c4c70..0f436e752 100644 --- a/packages/ui/src/assets/icons/index.ts +++ b/packages/ui/src/assets/icons/index.ts @@ -1,2 +1,3 @@ export * from './generated'; export * from './Svg'; +export * from './MimDefaultWallet'; diff --git a/packages/ui/src/assets/images/common/bgClockIcon.svg b/packages/ui/src/assets/images/common/bgClockIcon.svg new file mode 100644 index 000000000..ee78a4938 --- /dev/null +++ b/packages/ui/src/assets/images/common/bgClockIcon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/packages/ui/src/assets/images/common/clear-device-data.png b/packages/ui/src/assets/images/common/clear-device-data.png new file mode 100644 index 000000000..6e807aadd Binary files /dev/null and b/packages/ui/src/assets/images/common/clear-device-data.png differ diff --git a/packages/ui/src/assets/images/common/confirm-device-settings.png b/packages/ui/src/assets/images/common/confirm-device-settings.png new file mode 100644 index 000000000..164c9604f Binary files /dev/null and b/packages/ui/src/assets/images/common/confirm-device-settings.png differ diff --git a/packages/ui/src/assets/images/common/confirm-on-device.png b/packages/ui/src/assets/images/common/confirm-on-device.png new file mode 100644 index 000000000..100f4bd8e Binary files /dev/null and b/packages/ui/src/assets/images/common/confirm-on-device.png differ diff --git a/packages/ui/src/assets/images/common/cypherock-cover-icon.svg b/packages/ui/src/assets/images/common/cypherock-cover-icon.svg new file mode 100644 index 000000000..1c8e1c753 --- /dev/null +++ b/packages/ui/src/assets/images/common/cypherock-cover-icon.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/dash-wallet-default-bg-icon.svg b/packages/ui/src/assets/images/common/dash-wallet-default-bg-icon.svg new file mode 100644 index 000000000..ad1085068 --- /dev/null +++ b/packages/ui/src/assets/images/common/dash-wallet-default-bg-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/images/common/dash-wallet-hover-bg-icon.svg b/packages/ui/src/assets/images/common/dash-wallet-hover-bg-icon.svg new file mode 100644 index 000000000..816f61e2c --- /dev/null +++ b/packages/ui/src/assets/images/common/dash-wallet-hover-bg-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/images/common/expire-hover-wallet-icon.svg b/packages/ui/src/assets/images/common/expire-hover-wallet-icon.svg new file mode 100644 index 000000000..386c143ed --- /dev/null +++ b/packages/ui/src/assets/images/common/expire-hover-wallet-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/images/common/gold-hover-wallet-icon.svg b/packages/ui/src/assets/images/common/gold-hover-wallet-icon.svg new file mode 100644 index 000000000..ad1085068 --- /dev/null +++ b/packages/ui/src/assets/images/common/gold-hover-wallet-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/images/common/gold-tick.svg b/packages/ui/src/assets/images/common/gold-tick.svg new file mode 100644 index 000000000..8de784345 --- /dev/null +++ b/packages/ui/src/assets/images/common/gold-tick.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/index.ts b/packages/ui/src/assets/images/common/index.ts index bd4ad8690..f6becb521 100644 --- a/packages/ui/src/assets/images/common/index.ts +++ b/packages/ui/src/assets/images/common/index.ts @@ -5,6 +5,7 @@ import arrowGoldenForward from './arrow-golden-forward.svg'; import arrowWhiteBackward from './arrow-white-backward.svg'; import asideIcon from './aside.svg'; import backIcon from './back.svg'; +import bgClockIcon from './bgClockIcon.svg'; import binanceIcon from './binance.svg'; import bitcoinIcon from './bitcoin-icon.svg'; import blockIcon from './Block-unselectedsvg.svg'; @@ -13,13 +14,19 @@ import bnbChainIcon from './bnb-chain.svg'; import cardTapAsideImage from './CardTapAsideImage.png'; import checkIcon from './check.svg'; import circledCheckIcon from './circled-check-icon.svg'; +import clearDeviceData from './clear-device-data.png'; import closeIcon from './close.svg'; +import confirmDeviceSettings from './confirm-device-settings.png'; +import confirmOnDevice from './confirm-on-device.png'; import confirmIcon from './confirm.svg'; import goldCopyIcon from './copy-gold.svg'; import copyIcon from './copy.png'; import createWalletGraphics from './create-wallet-graphics.png'; +import cypherockCoverIcon from './cypherock-cover-icon.svg'; import cypherockRedIcon from './cypherock-red.svg'; import cysyncLockedLogo from './cysync-locked.svg'; +import dashWalletDefaultBgIcon from './dash-wallet-default-bg-icon.svg'; +import dashWalletHoverBgIcon from './dash-wallet-hover-bg-icon.svg'; import deviceAuthAsideImage from './DeviceAuthAside.png'; import { DeviceUpdateIcon } from './DeviceUpdateIcon'; import disconnectedIcon from './disconnected.svg'; @@ -28,8 +35,11 @@ import emailIconOutlined from './email.svg'; import errorIcon from './error.svg'; import ethereumIcon from './ethereum-icon.svg'; import etheriumBlueIcon from './etherium-blue.svg'; +import expireHoverWalletIcon from './expire-hover-wallet-icon.svg'; import { FailIcon } from './FailIcon'; +import goldHoverWalletIcon from './gold-hover-wallet-icon.svg'; import goldSendIcon from './gold-send.svg'; +import goldTick from './gold-tick.svg'; import goldFail from './gold_fail.svg'; import greenTick from './green-tick.svg'; import halfLoaderGold from './half-loader-gold.svg'; @@ -44,10 +54,17 @@ import loaderIcon from './loader.png'; import cysyncLogoBig from './logo-big.svg'; import cysyncLogoSmall from './logo-small.png'; import LogoOutlinedAsideImage from './LogoOutlineAside.png'; +import manyInManyBgImage from './mim-bg.svg'; +import manyInManyHoverBgImage from './mim-hover-bg.svg'; +import oneInMany1Default from './oneInmany1Default.svg'; +import oneInMany1Hover from './oneInmany1Hover.svg'; +import oneInMany2Default from './oneInMany2Default.svg'; +import oneInMany2Hover from './oneInMany2Hover.svg'; import openExternalLink from './open-external-link.svg'; import qrImage from './qr.png'; import qrcodeIcon from './qrcode.svg'; import questionMarkGoldIcon from './question-gold.svg'; +import questionMarkEllipseIcon from './question-mark-ellipse-icon.svg'; import questionMarkIcon from './question-mark.svg'; import recoverWalletFromSeedphraseGraphics from './recover-wallet-from-seedphrase-graphics.png'; import recoverWalletIcon from './recover-wallet.png'; @@ -57,21 +74,34 @@ import { ServerErrorIcon } from './ServerErrorIcon'; import settingsIcon from './settings.svg'; import { SettingsWrongIcon } from './SettingsWrongIcon'; import shieldAlert from './shield-alert.svg'; +import silverTickIcon from './silver-tick-icon.png'; import sliderThumbIcon from './slider-thumb.svg'; import solanaIcon from './solana.svg'; import spinnerGoldIcon from './spinner-gold.svg'; import successIcon from './success.svg'; +import syncIcon from './sync-icon.svg'; import tetherIcon from './tether.svg'; import triangleInverseIcon from './triangle-inverse-icon.svg'; import triangleGreyIcon from './triangleGrey.svg'; import UniSwapLogo from './UniSwapLogo.svg'; import usbIcon from './usb.svg'; import verifyCoinIcon from './verify-coin.svg'; +import verifyOnDevice from './verify-on-device.svg'; +import verifyPinOnDevice from './verify-pin-on-device.png'; +import WalletDefaultExpiredIcon from './wallet-default-expired.svg'; +import WalletDefaultPendingIcon from './wallet-default-pending.svg'; import walletErrorIcon from './wallet-error.svg'; +import WalletHoverExpiredIcon from './wallet-hover-expired.svg'; +import WalletHoverPendingIcon from './wallet-hover-pending.svg'; +import WalletHoverSilverBgIcon from './wallet-silver-hover-bg.svg'; import walletIcon from './wallet.svg'; import warningIcon from './warning.svg'; export { + WalletDefaultExpiredIcon, + WalletDefaultPendingIcon, + WalletHoverExpiredIcon, + WalletHoverPendingIcon, addWalletIcon, walletIcon, tetherIcon, @@ -144,4 +174,26 @@ export { UniSwapLogo, createWalletGraphics, recoverWalletFromSeedphraseGraphics, + goldHoverWalletIcon, + WalletHoverSilverBgIcon, + dashWalletDefaultBgIcon, + expireHoverWalletIcon, + dashWalletHoverBgIcon, + bgClockIcon, + oneInMany1Default, + oneInMany1Hover, + oneInMany2Hover, + oneInMany2Default, + manyInManyBgImage, + manyInManyHoverBgImage, + questionMarkEllipseIcon, + syncIcon, + cypherockCoverIcon, + goldTick, + confirmOnDevice, + silverTickIcon, + verifyOnDevice, + verifyPinOnDevice, + clearDeviceData, + confirmDeviceSettings, }; diff --git a/packages/ui/src/assets/images/common/mim-bg.svg b/packages/ui/src/assets/images/common/mim-bg.svg new file mode 100644 index 000000000..c32849cd6 --- /dev/null +++ b/packages/ui/src/assets/images/common/mim-bg.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/mim-hover-bg.svg b/packages/ui/src/assets/images/common/mim-hover-bg.svg new file mode 100644 index 000000000..737e172bb --- /dev/null +++ b/packages/ui/src/assets/images/common/mim-hover-bg.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/oneInMany2Default.svg b/packages/ui/src/assets/images/common/oneInMany2Default.svg new file mode 100644 index 000000000..1da27194b --- /dev/null +++ b/packages/ui/src/assets/images/common/oneInMany2Default.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/oneInMany2Hover.svg b/packages/ui/src/assets/images/common/oneInMany2Hover.svg new file mode 100644 index 000000000..381171b08 --- /dev/null +++ b/packages/ui/src/assets/images/common/oneInMany2Hover.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/oneInmany1Default.svg b/packages/ui/src/assets/images/common/oneInmany1Default.svg new file mode 100644 index 000000000..0e48b5539 --- /dev/null +++ b/packages/ui/src/assets/images/common/oneInmany1Default.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/oneInmany1Hover.svg b/packages/ui/src/assets/images/common/oneInmany1Hover.svg new file mode 100644 index 000000000..7a3eb41de --- /dev/null +++ b/packages/ui/src/assets/images/common/oneInmany1Hover.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/question-mark-ellipse-icon.svg b/packages/ui/src/assets/images/common/question-mark-ellipse-icon.svg new file mode 100644 index 000000000..9e3f2261a --- /dev/null +++ b/packages/ui/src/assets/images/common/question-mark-ellipse-icon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/silver-tick-icon.png b/packages/ui/src/assets/images/common/silver-tick-icon.png new file mode 100644 index 000000000..c991caf49 Binary files /dev/null and b/packages/ui/src/assets/images/common/silver-tick-icon.png differ diff --git a/packages/ui/src/assets/images/common/sync-icon.svg b/packages/ui/src/assets/images/common/sync-icon.svg new file mode 100644 index 000000000..0404793be --- /dev/null +++ b/packages/ui/src/assets/images/common/sync-icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/verify-on-device.svg b/packages/ui/src/assets/images/common/verify-on-device.svg new file mode 100644 index 000000000..724d9085f --- /dev/null +++ b/packages/ui/src/assets/images/common/verify-on-device.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/verify-pin-on-device.png b/packages/ui/src/assets/images/common/verify-pin-on-device.png new file mode 100644 index 000000000..ea07c40c7 Binary files /dev/null and b/packages/ui/src/assets/images/common/verify-pin-on-device.png differ diff --git a/packages/ui/src/assets/images/common/wallet-default-expired.svg b/packages/ui/src/assets/images/common/wallet-default-expired.svg new file mode 100644 index 000000000..87993da74 --- /dev/null +++ b/packages/ui/src/assets/images/common/wallet-default-expired.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/images/common/wallet-default-pending.svg b/packages/ui/src/assets/images/common/wallet-default-pending.svg new file mode 100644 index 000000000..87993da74 --- /dev/null +++ b/packages/ui/src/assets/images/common/wallet-default-pending.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/ui/src/assets/images/common/wallet-hover-expired.svg b/packages/ui/src/assets/images/common/wallet-hover-expired.svg new file mode 100644 index 000000000..c1d932ef5 --- /dev/null +++ b/packages/ui/src/assets/images/common/wallet-hover-expired.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/wallet-hover-pending.svg b/packages/ui/src/assets/images/common/wallet-hover-pending.svg new file mode 100644 index 000000000..e98e4a780 --- /dev/null +++ b/packages/ui/src/assets/images/common/wallet-hover-pending.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/ui/src/assets/images/common/wallet-silver-hover-bg.svg b/packages/ui/src/assets/images/common/wallet-silver-hover-bg.svg new file mode 100644 index 000000000..4560132f9 --- /dev/null +++ b/packages/ui/src/assets/images/common/wallet-silver-hover-bg.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/packages/ui/src/components/GlobalStyles.tsx b/packages/ui/src/components/GlobalStyles.tsx index 2ff85c816..9a9a68564 100644 --- a/packages/ui/src/components/GlobalStyles.tsx +++ b/packages/ui/src/components/GlobalStyles.tsx @@ -59,6 +59,64 @@ export const GlobalStyles: React.FC = () => ( + + {/* For Many In Many Component */} + + + + + + + + + + + + + + + + + + + + + + + ); diff --git a/packages/ui/src/components/atoms/Button.tsx b/packages/ui/src/components/atoms/Button.tsx index 60b44f080..f9cda5b86 100644 --- a/packages/ui/src/components/atoms/Button.tsx +++ b/packages/ui/src/components/atoms/Button.tsx @@ -5,18 +5,21 @@ import { Throbber } from './Throbber'; import { SvgProps } from '../../assets'; import { svgGradients } from '../GlobalStyles'; -import { UtilsProps, goldenGradient, utils } from '../utils'; +import { UtilsProps, goldenGradient, silverGradient, utils } from '../utils'; type ButtonVariant = | 'primary' | 'secondary' | 'secondaryLight' + | 'silver' | 'warning' | 'danger' | 'text' | 'icon' | 'none'; + type ButtonSize = 'lg' | 'md' | 'sm'; + export interface ButtonProps extends UtilsProps, React.ButtonHTMLAttributes { @@ -151,6 +154,10 @@ const buttonVariantCssMap: Record> = { border: none; padding: 0; `, + silver: css` + ${silverGradient('background')}; + border: 1px solid transparent; + `, }; const buttonStyle = css` diff --git a/packages/ui/src/components/atoms/Chip.tsx b/packages/ui/src/components/atoms/Chip.tsx index 4f5397f02..cfe0923af 100644 --- a/packages/ui/src/components/atoms/Chip.tsx +++ b/packages/ui/src/components/atoms/Chip.tsx @@ -1,13 +1,20 @@ import React, { FC } from 'react'; import styled from 'styled-components'; -import { FontProps, SpacingProps, font, spacing } from '../utils'; +import { + FontProps, + SpacingProps, + font, + silverGradient, + spacing, +} from '../utils'; interface ChipProps extends SpacingProps, FontProps { children: React.ReactNode; + $gradient?: boolean; } -const ChipStyle = styled.div` +const ChipStyle = styled.div` display: flex; padding: 1px 6px; gap: var(--0-px, 0px); @@ -16,8 +23,13 @@ const ChipStyle = styled.div` border: 1px solid ${({ theme }) => theme.palette.border.muted}; ${spacing} ${font} + ${({ $gradient }) => ($gradient ? silverGradient('background') : '')} `; export const Chip: FC = ({ children, ...props }) => ( {children} ); + +Chip.defaultProps = { + $gradient: undefined, +}; diff --git a/packages/ui/src/components/atoms/Container.tsx b/packages/ui/src/components/atoms/Container.tsx index 35879d761..800834000 100644 --- a/packages/ui/src/components/atoms/Container.tsx +++ b/packages/ui/src/components/atoms/Container.tsx @@ -26,9 +26,10 @@ export interface ContainerProps children?: ReactNode; size?: 'lg'; $noFlex?: boolean; + $variant?: 'div' | 'span' | 'p'; } -const ContainerStyle = styled.div` +const containerStyles = css` ${props => !props.$noFlex && css` @@ -44,14 +45,33 @@ const ContainerStyle = styled.div` ${height} `; -export const Container: FC = ({ children, ...props }) => ( - {children} -); +const ContainerStyleDiv = styled.div` + ${containerStyles} +`; +const ContainerStyleP = styled.div` + ${containerStyles} +`; +const ContainerStyleSpan = styled.div` + ${containerStyles} +`; + +const variantMap = { + div: ContainerStyleDiv, + p: ContainerStyleP, + span: ContainerStyleSpan, +}; + +export const Container: FC = ({ children, ...props }) => { + const ContainerStyle = variantMap[props.$variant ?? 'div']; + + return {children}; +}; Container.defaultProps = { children: null, size: 'lg', $noFlex: false, + $variant: 'div', }; export const FlexGapContainer = styled(Container)` diff --git a/packages/ui/src/components/atoms/ExternalLink.tsx b/packages/ui/src/components/atoms/ExternalLink.tsx new file mode 100644 index 000000000..cb5c14e8d --- /dev/null +++ b/packages/ui/src/components/atoms/ExternalLink.tsx @@ -0,0 +1,89 @@ +import React from 'react'; + +import { Bullet } from './Bullet'; +import { Container } from './Container'; +import { Flex } from './Flex'; +import { LangDisplay } from './LangDisplay'; +import { Typography, TypographyColor } from './Typography'; + +import { OpenExternalLinkIcon } from '../../assets'; +import { useTheme } from '../../themes'; +import { BgColor, UtilsProps } from '../utils'; + +export type ExternalLinkVariantTypes = 'disabled' | 'golden'; + +export interface ExternalLinkProps extends UtilsProps { + type?: ExternalLinkVariantTypes; + icon?: React.ReactNode; + text: string; + href: string; +} + +const bgColorMap: Record = { + golden: 'golden', + disabled: 'slate', +}; + +const textColorMap: Record = { + golden: 'black', + disabled: 'disabled', +}; + +export const ExternalLink: React.FC = ({ + type, + icon, + text, + href, + ...restProps +}) => { + const theme = useTheme(); + const fillColorMap: Record = { + golden: theme.palette.text.black, + disabled: theme.palette.text.disabled, + default: theme.palette.text.white, + }; + + let bgColor: BgColor = 'input'; + let textColor: TypographyColor = 'heading'; + let fillColor = fillColorMap.default; + + if (type) { + bgColor = bgColorMap[type] ?? 'input'; + textColor = textColorMap[type] ?? 'heading'; + fillColor = fillColorMap[type]; + } + + return ( + + + + {icon ?? } + + + + + + + + + + ); +}; + +ExternalLink.defaultProps = { + type: undefined, + icon: undefined, +}; diff --git a/packages/ui/src/components/atoms/Input/TextAreaInput.tsx b/packages/ui/src/components/atoms/Input/TextAreaInput.tsx deleted file mode 100644 index b2852bb47..000000000 --- a/packages/ui/src/components/atoms/Input/TextAreaInput.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React, { FC } from 'react'; -import styled from 'styled-components'; - -import { UtilsProps, utils } from '../../utils'; - -interface TextAreaInputProps extends UtilsProps { - value?: string; - onChange?: (val: string) => void; - onBlur?: (val: string) => void; - disabled?: boolean; - placeholder: string; -} - -const TextAreaInputStyle = styled.textarea` - position: relative; - width: 100%; - border: none; - padding-top: ${({ theme }) => theme.spacing.two.spacing}; - padding-bottom: ${({ theme }) => theme.spacing.two.spacing}; - padding-left: ${({ theme }) => theme.spacing.three.spacing}; - padding-right: ${({ theme }) => theme.spacing.three.spacing}; - background-color: ${({ theme }) => theme.palette.background.input}; - border-radius: ${({ theme }) => theme.spacing.one.spacing}; - font-size: ${({ theme }) => theme.spacing.two.spacing}; - margin-bottom: ${({ theme }) => theme.spacing.two.spacing}; - color: white; - height: 182px; - resize: none; - ::placeholder { - font-weight: 300; - font-size: 14px; - line-height: 21px; - color: #8b8682; - } - ${utils} -`; - -export const TextAreaInput: FC = ({ - onChange, - onBlur, - ...props -}) => ( - onChange?.(e.target.value)} - onBlur={e => onBlur?.(e.target.value)} - /> -); - -TextAreaInput.defaultProps = { - value: '', - onChange: undefined, - onBlur: undefined, - disabled: false, -}; diff --git a/packages/ui/src/components/atoms/LangDisplay.tsx b/packages/ui/src/components/atoms/LangDisplay.tsx index 2ffbbb4be..53ffdb6f8 100644 --- a/packages/ui/src/components/atoms/LangDisplay.tsx +++ b/packages/ui/src/components/atoms/LangDisplay.tsx @@ -1,27 +1,55 @@ +import { Marked } from 'marked'; import React from 'react'; export interface LangDisplayProps { text: string; variables?: object; $noPreWrap?: boolean; + $allowMarkdown?: boolean; } +const marked = new Marked({ + tokenizer: { + url: () => undefined, + }, + gfm: false, +}); + export const parseLangTemplate = (templateStr: string, variables = {}) => - templateStr.replace(/\${(.*?)}/g, (x, g) => (variables as any)[g] ?? ''); + templateStr.replace(/\${(.*?)}/g, (_, g) => (variables as any)[g] ?? ''); const BaseLangDisplay: React.FC = ({ text, variables, $noPreWrap, -}) => ( - - {parseLangTemplate(text, variables)} - -); + $allowMarkdown, +}) => { + let parsedText = parseLangTemplate(text, variables); + + if ($allowMarkdown) { + parsedText = marked.parseInline(parsedText) as string; + } + + return ( + + {$allowMarkdown ? undefined : parsedText} + + ); +}; BaseLangDisplay.defaultProps = { variables: undefined, $noPreWrap: undefined, + $allowMarkdown: undefined, }; export const LangDisplay = React.memo(BaseLangDisplay); diff --git a/packages/ui/src/components/atoms/NomineeMessage.tsx b/packages/ui/src/components/atoms/NomineeMessage.tsx new file mode 100644 index 000000000..5f15ba83c --- /dev/null +++ b/packages/ui/src/components/atoms/NomineeMessage.tsx @@ -0,0 +1,91 @@ +import React, { FC } from 'react'; +import styled from 'styled-components'; + +import { Flex } from './Flex'; +import { Typography, TypographyColor } from './Typography'; + +import { width, WidthProps } from '../utils'; + +export type NomineeMessageVariant = 'danger'; + +export interface NomineeMessageProps extends WidthProps { + label: string; + value?: string; + leading?: React.ReactNode; + trailing?: React.ReactNode; + variant?: NomineeMessageVariant; + withBackground?: boolean; + isHeader?: boolean; + centerText?: boolean; +} + +const StyledNomineeMessage = styled.div< + { $withBackground: boolean } & WidthProps +>` + display: flex; + min-width: 460px; + padding: 8px 16px; + justify-content: space-between; + align-items: flex-start; + background: ${({ $withBackground, theme }) => + $withBackground ? theme.palette.background.slate : 'none'}; + border-radius: 8px; + ${width} +`; + +const variantColorMap: Record = { + danger: 'error', +}; + +export const NomineeMessage: FC = ({ + label, + value, + leading, + trailing, + variant, + withBackground, + isHeader: withHeaderFont, + centerText, + ...props +}) => ( + + + {leading} + + {label} + + + {!centerText && ( + + + {value} + + {trailing} + + )} + +); + +NomineeMessage.defaultProps = { + value: undefined, + leading: undefined, + trailing: undefined, + variant: undefined, + withBackground: undefined, + isHeader: undefined, + centerText: false, +}; diff --git a/packages/ui/src/components/atoms/Tooltip/index.tsx b/packages/ui/src/components/atoms/Tooltip/index.tsx index a8ae04827..b077d2d78 100644 --- a/packages/ui/src/components/atoms/Tooltip/index.tsx +++ b/packages/ui/src/components/atoms/Tooltip/index.tsx @@ -20,10 +20,22 @@ export const Tooltip: React.FC = ({ }) => ( {text}} + overlay={ +
+ {text} +
+ } visible={text && isActive === true ? undefined : false} > - {children} +
{children}
); diff --git a/packages/ui/src/components/atoms/Typography.tsx b/packages/ui/src/components/atoms/Typography.tsx index 6e62b7408..324920d5f 100644 --- a/packages/ui/src/components/atoms/Typography.tsx +++ b/packages/ui/src/components/atoms/Typography.tsx @@ -33,7 +33,7 @@ interface HeadingProps $textAlign?: 'center' | 'left' | 'right'; $letterSpacing?: string | number; $userSelect?: 'all' | 'auto' | 'none' | 'text'; - $whiteSpace?: 'normal' | 'nowrap' | 'pre-wrap'; + $whiteSpace?: 'normal' | 'nowrap' | 'pre-wrap' | 'pre-line'; $textOverflow?: 'clip' | 'ellipsis' | 'fade'; $filter?: string; } @@ -86,7 +86,7 @@ const baseStyle = css` ? `${props.$letterSpacing}em` : props.$letterSpacing}; `} - + ${props => props.$whiteSpace !== undefined && css` @@ -119,7 +119,6 @@ const baseStyle = css` ${position}; ${flex}; ${display}; - ${flex}; `; const HeadingOneStyle = styled.h1` diff --git a/packages/ui/src/components/atoms/index.ts b/packages/ui/src/components/atoms/index.ts index 1636f66d9..6b91ecf19 100644 --- a/packages/ui/src/components/atoms/index.ts +++ b/packages/ui/src/components/atoms/index.ts @@ -3,7 +3,6 @@ export * from './Bullet'; export * from './Container'; export * from './Flex'; export * from './Image'; -export * from './Input'; export * from './Typography'; export * from './Indicator'; export * from './AsideContainer'; @@ -33,3 +32,5 @@ export * from './MainAppStyles'; export * from './Tooltip'; export * from './Video'; export * from './AdvanceTextDiaplay'; +export * from './NomineeMessage'; +export * from './ExternalLink'; diff --git a/packages/ui/src/components/molecules/Accordion.tsx b/packages/ui/src/components/molecules/Accordion.tsx new file mode 100644 index 000000000..1f99291ea --- /dev/null +++ b/packages/ui/src/components/molecules/Accordion.tsx @@ -0,0 +1,123 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import PropTypes from 'prop-types'; +import React, { FC } from 'react'; +import { styled } from 'styled-components'; + +import { AngleDown, InfoItalicsIcon } from '../../assets'; +import { Typography, Flex } from '../atoms'; +import { + FlexProps, + flex, + utils, + width, + WidthProps, + UtilsProps, +} from '../utils'; + +export interface AccordionProps extends WidthProps { + id: string; + title: string; + content: string; +} + +const HeaderTitleContainer = styled.div` + display: flex; + justify-content: flex-start; + align-items: center; + color: ${({ theme }) => theme.palette.text.message}; + height: auto; + width: 100%; +`; + +const DetailsContainer = styled.div` + border-top: 0px solid ${({ theme }) => theme.palette.border.separator}; + transition: max-height ease-in-out 0.5s, border-top step-end 0.5s; + max-height: 0px; +`; + +const ArrowContainer = styled(Flex)` + transition: transform ease-in-out 0.5s; +`; + +const HeaderContainer = styled.label` + display: flex; + ${flex} + ${utils} +`; + +const StyledContainer = styled.div` + border-radius: 8px; + overflow: hidden; + width: 720px; + background: ${({ theme }) => theme.palette.background.separatorSecondary}; + border: 1px solid ${({ theme }) => theme.palette.border.separator}; + line-height: 18px; + text-align: left; + + ${width} + + input { + display: none; + } + + input:checked ~ ${HeaderContainer} ${ArrowContainer} { + transform: rotate(180deg); + } + + input:checked ~ ${DetailsContainer} { + max-height: 100vh; + transition: max-height ease-in-out 1s, border-top step-start 1s; + border-top: 1px solid ${({ theme }) => theme.palette.border.separator}; + } +`; + +export const Accordion: FC = ({ + id, + title, + content, + ...restProps +}) => { + const inputId = `accordion-${id}`; + + return ( + + + + + + + {title} + + + + + + + + + {content} + + + + ); +}; + +Accordion.propTypes = { + id: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + content: PropTypes.string.isRequired, +}; diff --git a/packages/ui/src/components/molecules/Asides/MilestoneAside.tsx b/packages/ui/src/components/molecules/Asides/MilestoneAside.tsx index 7787d1eb2..b36dc4e51 100644 --- a/packages/ui/src/components/molecules/Asides/MilestoneAside.tsx +++ b/packages/ui/src/components/molecules/Asides/MilestoneAside.tsx @@ -42,7 +42,9 @@ export const MilestoneAside: FC<{ }> = ({ milestones, activeTab, heading, skippedTabs }) => ( {heading ? ( - {heading} + + {heading} + ) : ( logo )} diff --git a/packages/ui/src/components/molecules/CardTapList.tsx b/packages/ui/src/components/molecules/CardTapList.tsx index 4a75500ef..159c06607 100644 --- a/packages/ui/src/components/molecules/CardTapList.tsx +++ b/packages/ui/src/components/molecules/CardTapList.tsx @@ -3,25 +3,37 @@ import { styled } from 'styled-components'; import { Flex, Indicator, LangDisplay, Typography } from '../atoms'; +type VariantType = 'default' | 'muted'; + interface CardTapItemProps { text: string; currentState: number; totalState: number; currentFailed?: boolean; + variant?: VariantType; } interface CardTapListProps { items: CardTapItemProps[]; + variant?: VariantType; } -const getBgColor = (currentState: number, totalState: number) => { - if (currentState < 0) return 'inputSecondary'; +const getBgColor = ( + currentState: number, + totalState: number, + variant: VariantType, +) => { + if (currentState < 0 || variant === 'muted') return 'inputSecondary'; if (currentState < totalState) return 'gold'; return 'successSecondary'; }; -const getTextColor = (currentState: number, totalState: number) => { - if (currentState < 0) return 'muted'; +const getTextColor = ( + currentState: number, + totalState: number, + variant: VariantType, +) => { + if (currentState < 0 || variant === 'muted') return 'muted'; if (currentState < totalState) return 'gold'; return 'success'; }; @@ -64,6 +76,7 @@ const CardTapListItem: React.FC = ({ currentState, totalState, currentFailed, + variant = 'default', }) => { const indicators = Array(totalState) .fill(0) @@ -76,17 +89,16 @@ const CardTapListItem: React.FC = ({ )); return ( - + - {indicators} @@ -95,8 +107,13 @@ const CardTapListItem: React.FC = ({ CardTapListItem.defaultProps = { currentFailed: false, + variant: 'default', }; -export const CardTapList: React.FC = ({ items }) => ( + +export const CardTapList: React.FC = ({ + items, + variant = 'default', +}) => ( {items.map(({ text, currentState, totalState, currentFailed }, i) => ( = ({ items }) => ( currentState={currentState} totalState={totalState} currentFailed={currentFailed} + variant={variant} /> ))} ); + +CardTapList.defaultProps = { + variant: 'default', +}; diff --git a/packages/ui/src/components/molecules/CouponInput.tsx b/packages/ui/src/components/molecules/CouponInput.tsx new file mode 100644 index 000000000..c454e3431 --- /dev/null +++ b/packages/ui/src/components/molecules/CouponInput.tsx @@ -0,0 +1,168 @@ +import React from 'react'; +import styled, { useTheme } from 'styled-components'; + +import { DeleteIcon } from '../../assets'; +import { Button, Flex, Throbber, Typography } from '../atoms'; +import { goldenGradient, UtilsProps, width, WidthProps } from '../utils'; + +interface ApplyButtonProps + extends UtilsProps, + React.ButtonHTMLAttributes { + $isInvalid?: boolean; +} + +const Container = styled.div` + display: flex; + width: 320px; + padding: 0px; + justify-content: space-between; + align-items: center; + height: 45px; + ${width} +`; + +const InputField = styled.input<{ $isInvalid: boolean }>` + display: flex; + padding: 12px 16px; + font-size: 14px; + align-items: center; + height:45px; + gap: 16px + border-color: transparent; + flex: 1 0 0; + background: ${({ theme }) => theme.palette.background.separatorSecondary}; + color: ${({ theme }) => theme.palette.text.white}; + + outline: none; + + &::placeholder { + color: ${({ theme }) => theme.palette.text.muted}; + } + + ::-webkit-input-placeholder { + color: ${({ theme }) => theme.palette.text.muted}; + } + border-radius: 8px 0px 0px 8px; + + border: 1px solid ${({ theme, $isInvalid }) => + $isInvalid + ? theme.palette.background.danger + : theme.palette.border.separator}; + border-right: none; +`; + +const ApplyButton = styled.button` + border-radius: 0px 8px 8px 0px; + height: 45px; + color: ${({ theme, disabled }) => + disabled ? theme.palette.text.disabled : theme.palette.text.black}; + ${props => + props.disabled + ? `background: ${props.theme.palette.background.disabled};` + : goldenGradient('background')} + padding: 12px 16px; + cursor: pointer; + border: none; + border: 1px solid + ${({ theme, $isInvalid }) => + $isInvalid ? theme.palette.background.danger : 'transparent'}; + border-left: none; +`; + +export interface CouponInputProps extends WidthProps { + isApplied: boolean; + isInvalid: boolean; + value: string; + onApply: () => void; + onDelete: () => void; + onChange: (value: string) => void; + appliedText: string; + applyButtonText: string; + placeholderText: string; + disabled?: boolean; + isLoading?: boolean; + couponLength?: number; +} + +export const CouponInput: React.FC = ({ + isApplied, + isInvalid, + value, + onApply, + onDelete, + onChange, + appliedText, + placeholderText, + applyButtonText, + disabled, + isLoading, + couponLength, + ...restProps +}) => ( + + {!isApplied ? ( + <> + onChange(event.target.value)} + $isInvalid={isInvalid} + maxLength={couponLength} + /> + + {isLoading ? : applyButtonText} + + + ) : ( + + + + {appliedText} + + + {value} + + + + + + ); +}; +PlanCard.defaultProps = { + popularTagText: undefined, +}; diff --git a/packages/ui/src/components/molecules/Prefabs/EditButton.tsx b/packages/ui/src/components/molecules/Prefabs/EditButton.tsx new file mode 100644 index 000000000..8d09c0f39 --- /dev/null +++ b/packages/ui/src/components/molecules/Prefabs/EditButton.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import { Button, Typography } from '../../atoms'; + +export interface EditButtonProps { + text: string; + onClick?: () => void; +} + +export const EditButton: React.FC = ({ text, onClick }) => ( + +); + +EditButton.defaultProps = { + onClick: undefined, +}; diff --git a/packages/ui/src/components/molecules/Prefabs/QuestionMarkButton.tsx b/packages/ui/src/components/molecules/Prefabs/QuestionMarkButton.tsx index b995bb3fe..45effdde7 100644 --- a/packages/ui/src/components/molecules/Prefabs/QuestionMarkButton.tsx +++ b/packages/ui/src/components/molecules/Prefabs/QuestionMarkButton.tsx @@ -3,8 +3,18 @@ import React from 'react'; import { Button, Typography } from '../../atoms'; export const QuestionMarkButton: React.FC = () => ( - diff --git a/packages/ui/src/components/molecules/Prefabs/index.ts b/packages/ui/src/components/molecules/Prefabs/index.ts index ef84059db..7cba2bd4b 100644 --- a/packages/ui/src/components/molecules/Prefabs/index.ts +++ b/packages/ui/src/components/molecules/Prefabs/index.ts @@ -2,3 +2,4 @@ export * from './BackButton'; export * from './EmailDisplay'; export * from './HelpButton'; export * from './QuestionMarkButton'; +export * from './EditButton'; diff --git a/packages/ui/src/components/molecules/Reminder.tsx b/packages/ui/src/components/molecules/Reminder.tsx new file mode 100644 index 000000000..15f14fa57 --- /dev/null +++ b/packages/ui/src/components/molecules/Reminder.tsx @@ -0,0 +1,189 @@ +import React, { FC, useState } from 'react'; +import styled from 'styled-components'; + +import { ClockIcon, InformationIcon, bgClockIcon } from '../../assets'; +import { useTheme } from '../../themes'; +import { Typography, Flex } from '../atoms'; +import { svgGradients } from '../GlobalStyles'; +import { WidthProps, width } from '../utils'; + +const StyledContainer = styled.div< + WidthProps & { $isSelected: boolean; $isHover: boolean } +>` + position: relative; + background: ${props => + !props.$isSelected + ? `linear-gradient(273deg, rgba(96, 58, 23, 0.20) 1.52%, rgba(0, 0, 0, 0.00) 52.42%), #2A2A27` + : props.theme.palette.background.cardSelected}; + border-radius: 8px; + overflow: hidden; + cursor: pointer; + width: 348px; + height: 53px; + color: ${({ theme }) => theme.palette.bullet.white}; + z-index: 1; + box-shadow: ${props => + props.$isSelected + ? `0px 0px 12px 4px ${props.theme.palette.shadow.selected} inset` + : `0px 0px 12px 4px ${props.theme.palette.shadow.selected}`}; + border: 1px solid + ${props => + props.$isSelected + ? `${props.theme.palette.border.selected}` + : 'transparent'}; + + ${width} + + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: ${props => + props.$isSelected + ? '' + : `url(${bgClockIcon}), url(${bgClockIcon}), url(${bgClockIcon})`}, + url(${bgClockIcon}), url(${bgClockIcon}), url(${bgClockIcon}); + background-size: 40px 40px; + background-repeat: no-repeat; + background-position: -20px -15px, 30px 30px, 80px -30px, 140px 30px, 200px 0, + 270px 30px; + z-index: -1; + animation: ${props => + !props.$isSelected && props.$isHover + ? 'moveBackgrounds 1s ease-in-out forwards' + : ''}; + } + + &:hover { + border-radius: var(--8-px, 8px); + background: ${props => + props.$isSelected + ? '' + : `linear-gradient(93deg, rgba(96, 58, 23, 0.20) 0%, rgba(0, 0, 0, 0.00) 52.46%), #332F2D`}; + &:before { + animation: moveBackgrounds 1s ease-in-out forwards; + } + } + + &:not(:hover):before { + animation: moveBackgroundsBack 1s ease-in-out forwards; + } + + @keyframes moveBackgrounds { + 0% { + background-position-y: -15px, 30px, -30px, 30px, 0, 30px; + background-position-x: -20px, 30px, 80px, 140px, 200px, 270px; + } + 100% { + background-position-y: 30px, -30px, 30px, -15px, 30px, -30px; + background-position-x: -20px, 50px, 110px, 160px, 230px, 290px; + } + } + + @keyframes moveBackgroundsBack { + 0% { + background-position-y: 30px, -30px, 30px, -15px, 30px, -30px; + background-position-x: -20px, 50px, 110px, 160px, 230px, 290px; + } + 100% { + background-position-y: -15px, 30px, -30px, 30px, 0, 30px; + background-position-x: -20px, 30px, 80px, 140px, 200px, 270px; + } + } +`; + +const DisableContainer = styled.div` + position: relative; + border-radius: 8px; + overflow: hidden; + width: 348px; + height: 53px; + background: ${({ theme }) => theme.palette.background.cardDisabled}; + display: flex; + justify-content: left; + align-items: center; + cursor: not-allowed; + ${width} +`; + +const ClockInfoContainer = styled.div` + position: absolute; + display: flex; + justify-content: center; + top: 50%; + right: 18px; + transform: translateY(-50%); +`; + +const DateLabel = styled(Typography)` + font-size: 14px; + font-weight: 700; + line-height: 21px; + text-align: left; + color: ${({ theme }) => theme.palette.bullet.white}; +`; + +const DisabledDate = styled(Typography)` + font-size: 14px; + font-weight: 400; + line-height: 21px; + text-align: left; + color: ${({ theme }) => theme.palette.background.separator}; +`; + +export interface ReminderProps extends WidthProps { + date: string; + disabled: boolean; + isSelected: boolean; + onClick: () => void; +} + +export const Reminder: FC = ({ + date, + disabled, + onClick, + isSelected, + ...restProps +}) => { + const [isHover, setIsHover] = useState(false); + const theme = useTheme(); + + return !disabled ? ( + setIsHover(true)} + onClick={onClick} + onMouseLeave={() => setIsHover(false)} + $isSelected={isSelected} + $isHover={isHover} + {...restProps} + > + + + {date} + + + ) : ( + + + + {date} + + + + + + ); +}; diff --git a/packages/ui/src/components/molecules/Send/FeesInput.tsx b/packages/ui/src/components/molecules/Send/FeesInput.tsx index 347dfa707..63818a75a 100644 --- a/packages/ui/src/components/molecules/Send/FeesInput.tsx +++ b/packages/ui/src/components/molecules/Send/FeesInput.tsx @@ -2,7 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react'; import { CustomInputSend } from './RecipientAddress'; -import { Input, Typography } from '../../atoms'; +import { Typography } from '../../atoms'; +import { Input } from '../Input'; interface FeesInputProps { value: string; diff --git a/packages/ui/src/components/molecules/Send/RecipientAddress.tsx b/packages/ui/src/components/molecules/Send/RecipientAddress.tsx index f48c6e39c..73eaa4308 100644 --- a/packages/ui/src/components/molecules/Send/RecipientAddress.tsx +++ b/packages/ui/src/components/molecules/Send/RecipientAddress.tsx @@ -2,7 +2,8 @@ import React from 'react'; import styled from 'styled-components'; import { PasteIcon } from '../../..'; -import { Button, Flex, Input, Throbber, Typography } from '../../atoms'; +import { Button, Flex, Throbber, Typography } from '../../atoms'; +import { Input } from '../Input'; interface RecipientAddressProps { text?: string; diff --git a/packages/ui/src/components/molecules/Table/TableSearch.tsx b/packages/ui/src/components/molecules/Table/TableSearch.tsx index 78075434b..9263ee041 100644 --- a/packages/ui/src/components/molecules/Table/TableSearch.tsx +++ b/packages/ui/src/components/molecules/Table/TableSearch.tsx @@ -2,8 +2,9 @@ import React, { FC } from 'react'; import styled, { css } from 'styled-components'; import { CrossMark, SearchIcon } from '../../../assets'; -import { Input, Typography } from '../../atoms'; +import { Typography } from '../../atoms'; import { goldenGradient, utils, UtilsProps } from '../../utils'; +import { Input } from '../Input'; export interface TableSearchProps { placeholder?: string; diff --git a/packages/ui/src/components/molecules/index.ts b/packages/ui/src/components/molecules/index.ts index 5890307f7..3c7aa5b31 100644 --- a/packages/ui/src/components/molecules/index.ts +++ b/packages/ui/src/components/molecules/index.ts @@ -40,4 +40,15 @@ export * from './Topbar'; export * from './UpdateBar'; export * from './WallectConnect'; export * from './WalletDialogMainContainer'; +export * from './DashboardWallet'; +export * from './ManyInMany'; +export * from './Reminder'; +export * from './Accordion'; export * from './SimpleJsonView'; +export * from './OTPInput'; +export * from './OneInMany'; +export * from './CouponInput'; +export * from './PlanCard'; +export * from './DetailsCard'; +export * from './Payment'; +export * from './Input'; diff --git a/packages/ui/src/components/utils/Gradient.tsx b/packages/ui/src/components/utils/Gradient.tsx index e9a93d619..4d1e11817 100644 --- a/packages/ui/src/components/utils/Gradient.tsx +++ b/packages/ui/src/components/utils/Gradient.tsx @@ -51,3 +51,52 @@ export const goldenGradient = (cssProperty: 'background' | 'color') => { return null; } }; + +export const silverGradient = (cssProperty: 'background' | 'color') => { + switch (cssProperty) { + case 'background': + return css` + @property --a { + syntax: ''; + inherits: false; + initial-value: 90deg; + } + transition: --a ${buttonAnimationData.duration} + ${buttonAnimationData.curve}; + ${cssProperty}: linear-gradient( + var(--a), + #A2ADB3 1.67%, + #F3F1F2 35.99%, + #BCC3C9 66.2%, + #DCDFE4 100% + ); + &:hover { + --a: 180deg; + } + `; + case 'color': + return css` + @property --a { + syntax: ''; + inherits: false; + initial-value: 90deg; + } + transition: --a ${buttonAnimationData.duration} + ${buttonAnimationData.curve}; + background: linear-gradient( + var(--a), + #a2adb3 1.67%, + #f3f1f2 35.99%, + #bcc3c9 66.2%, + #dcdfe4 100% + ); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + &:hover { + --a: 180deg; + } + `; + default: + return null; + } +}; diff --git a/packages/ui/src/components/utils/bgColor.styled.ts b/packages/ui/src/components/utils/bgColor.styled.ts index 0ad0764e3..79fee54e9 100644 --- a/packages/ui/src/components/utils/bgColor.styled.ts +++ b/packages/ui/src/components/utils/bgColor.styled.ts @@ -23,7 +23,15 @@ export type BgColor = | 'warning' | 'calendar' | 'container' - | 'error'; + | 'slate' + | 'slateDark' + | 'headlineLight' + | 'error' + | 'disabled' + | 'message' + | 'videoError' + | 'featureBanner'; + export interface BgColorProps { $bgColor?: BgColor; } @@ -65,11 +73,22 @@ export const bgColor = css` css` background: ${({ theme }) => theme.palette.background.separatorSecondary}; `} + ${props => + props.$bgColor === 'headlineLight' && + css` + background: ${({ theme }) => theme.palette.background.headlineLight}; + `} + ${props => + props.$bgColor === 'slate' && + css` + background: ${({ theme }) => theme.palette.background.slate}; + `} ${props => props.$bgColor === 'muted' && css` background: ${({ theme }) => theme.palette.text.muted}; `} + ${props => props.$bgColor === 'golden' && css` @@ -131,10 +150,35 @@ ${props => props.$bgColor === 'calendar' && css` background: ${({ theme }) => theme.palette.background.calendar}; + `} + ${props => + props.$bgColor === 'slateDark' && + css` + background: ${({ theme }) => theme.palette.background.slateDark}; `} ${props => props.$bgColor === 'error' && css` background: ${({ theme }) => theme.palette.background.error}; `} + ${props => + props.$bgColor === 'disabled' && + css` + background: ${({ theme }) => theme.palette.background.disabled}; + `} + ${props => + props.$bgColor === 'message' && + css` + background: ${({ theme }) => theme.palette.background.message}; + `} + ${props => + props.$bgColor === 'videoError' && + css` + background: ${({ theme }) => theme.palette.background.videoError}; + `} + ${props => + props.$bgColor === 'featureBanner' && + css` + background: ${({ theme }) => theme.palette.background.featureBanner}; + `} `; diff --git a/packages/ui/src/components/utils/border.styled.ts b/packages/ui/src/components/utils/border.styled.ts index d0cad5ecc..2e31e0232 100644 --- a/packages/ui/src/components/utils/border.styled.ts +++ b/packages/ui/src/components/utils/border.styled.ts @@ -26,7 +26,8 @@ export type BorderColor = | 'white' | 'list' | 'topbar' - | 'danger'; + | 'danger' + | 'success'; type BorderRadius = number | 'full' | string; type BorderStyle = 'dotted' | 'dashed' | 'solid' | 'double' | 'none'; diff --git a/packages/ui/src/components/utils/flex.styled.ts b/packages/ui/src/components/utils/flex.styled.ts index eced9027c..828191a15 100644 --- a/packages/ui/src/components/utils/flex.styled.ts +++ b/packages/ui/src/components/utils/flex.styled.ts @@ -27,6 +27,13 @@ type AlignSelfType = | 'initial' | 'inherit'; +export type FlexWrapType = + | 'nowrap' + | 'wrap' + | 'wrap-reverse' + | 'initial' + | 'inherit'; + const alignSelfObj: Record = { start: 'flex-start', end: 'flex-end', @@ -44,6 +51,7 @@ export interface FlexProps { shrink?: MediaQuery; $alignSelf?: MediaQuery; $flex?: MediaQuery; + $flexWrap?: MediaQuery; } const flexProperty = css` @@ -85,13 +93,13 @@ const gap = css` const grow = css` ${props => - props.grow && + props.grow !== undefined && generateCss(['flex-grow'], (item: number) => `${item}`, props.grow)} `; const shrink = css` ${props => - props.shrink && + props.shrink !== undefined && generateCss(['flex-shrink'], (item: number) => `${item}`, props.shrink)} `; @@ -105,6 +113,12 @@ const alignSelf = css` )} `; +const flexWrap = css` + ${props => + props.$flexWrap && + generateCss(['flex-wrap'], (item: string) => item, props.$flexWrap)} +`; + export const flex = css` ${justifyContent} ${align} @@ -114,4 +128,5 @@ export const flex = css` ${shrink} ${alignSelf} ${flexProperty} + ${flexWrap} `; diff --git a/packages/ui/src/components/utils/font.styled.ts b/packages/ui/src/components/utils/font.styled.ts index 558443749..ca46697a7 100644 --- a/packages/ui/src/components/utils/font.styled.ts +++ b/packages/ui/src/components/utils/font.styled.ts @@ -27,6 +27,8 @@ export interface FontProps { $fontSize?: MediaQuery; $fontFamily?: MediaQuery; $wordBreak?: MediaQuery; + $letterSpacing?: string | number; + $lineHeight?: MediaQuery; } export const fontWeightObj: Record = { @@ -69,4 +71,21 @@ export const font = css` ${props => props.$wordBreak && generateCss(['word-break'], val => val, props.$wordBreak)}; + + ${props => + props.$letterSpacing !== undefined && + css` + letter-spacing: ${typeof props.$letterSpacing === 'number' + ? `${props.$letterSpacing}em` + : props.$letterSpacing}; + `} + + ${props => + props.$lineHeight && + generateCss( + ['line-height'], + (item: number | string) => + typeof item === 'number' ? `${item}px` : item, + props.$lineHeight, + )} `; diff --git a/packages/ui/src/stories/atoms/Button.stories.tsx b/packages/ui/src/stories/atoms/Button.stories.tsx index 4c06daa82..1ca153323 100644 --- a/packages/ui/src/stories/atoms/Button.stories.tsx +++ b/packages/ui/src/stories/atoms/Button.stories.tsx @@ -27,6 +27,13 @@ export const Secondary: Story = { }, }; +export const Silver: Story = { + args: { + ...Primary.args, + variant: 'silver', + }, +}; + export const Warning: Story = { args: { ...Primary.args, diff --git a/packages/ui/src/stories/atoms/ExternalLink.stories.tsx b/packages/ui/src/stories/atoms/ExternalLink.stories.tsx new file mode 100644 index 000000000..a8e68f90f --- /dev/null +++ b/packages/ui/src/stories/atoms/ExternalLink.stories.tsx @@ -0,0 +1,20 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ExternalLink } from '../../components'; + +const meta: Meta = { + component: ExternalLink, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + href: 'https://www.cypherock.com', + text: 'Terms and Conditions', + $width: 400, + }, +}; diff --git a/packages/ui/src/stories/atoms/NomineeMessage.stories.tsx b/packages/ui/src/stories/atoms/NomineeMessage.stories.tsx new file mode 100644 index 000000000..5301c6d87 --- /dev/null +++ b/packages/ui/src/stories/atoms/NomineeMessage.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { EncryptedMessageIcon, MessageIcon } from '../../assets'; +import { EditButton, NomineeMessage } from '../../components'; + +const meta: Meta = { + component: NomineeMessage, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const EncryptedMessage: Story = { + args: { + label: 'Encrypted Message', + leading: , + trailing: , + withBackground: true, + }, +}; + +export const ExecutorMessage: Story = { + args: { + label: 'Executor Message', + leading: , + trailing: , + withBackground: true, + }, +}; diff --git a/packages/ui/src/stories/molecules/Accordion.stories.tsx b/packages/ui/src/stories/molecules/Accordion.stories.tsx new file mode 100644 index 000000000..87488cfba --- /dev/null +++ b/packages/ui/src/stories/molecules/Accordion.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Accordion } from '../../components'; + +const meta: Meta = { + component: Accordion, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + id: 'reminder-time', + title: 'What is reminder time?', + content: + 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime fuga magnam iure assumenda eligendi animi architecto facere quas dignissimos odio doloremque sapiente tenetur, totam temporibus. Accusantium ad a praesentium corporis mollitia magni dignissimos. Eos ut a exercitationem consequatur quod. Enim assumenda impedit maiores dolores nam doloremque eveniet quas quis culpa?', + }, +}; diff --git a/packages/ui/src/stories/molecules/CardTapList.stories.tsx b/packages/ui/src/stories/molecules/CardTapList.stories.tsx index 3289965c3..32e8fbb96 100644 --- a/packages/ui/src/stories/molecules/CardTapList.stories.tsx +++ b/packages/ui/src/stories/molecules/CardTapList.stories.tsx @@ -35,3 +35,17 @@ export const Failed: Story = { ], }, }; + +export const Variant: Story = { + args: { + items: [ + { + text: 'Tap this card', + currentState: 2, + totalState: 3, + currentFailed: true, + }, + ], + variant: 'muted', + }, +}; diff --git a/packages/ui/src/stories/molecules/CouponInput.stories.tsx b/packages/ui/src/stories/molecules/CouponInput.stories.tsx new file mode 100644 index 000000000..3efe3aa51 --- /dev/null +++ b/packages/ui/src/stories/molecules/CouponInput.stories.tsx @@ -0,0 +1,69 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { CouponInput } from '../../components'; + +const meta: Meta = { + component: CouponInput, + render: args => { + const [value, setValue] = React.useState(''); + const [applied, setApplied] = React.useState(args.isApplied || false); + + const onChange = (_value: string) => { + setValue(_value); + }; + + const onApply = () => { + if (value.trim() !== '') { + setApplied(true); + } + }; + + const onDelete = () => { + setValue(''); + setApplied(false); + }; + + return ( + + ); + }, + tags: ['autodocs'], +}; +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + appliedText: 'Coupon Applied:', + applyButtonText: 'APPLY', + placeholderText: 'Enter Coupon Code', + }, +}; + +export const AppliedState: Story = { + args: { + appliedText: 'Coupon Applied:', + applyButtonText: 'APPLY', + value: 'WW3vfAt', + isApplied: true, + placeholderText: 'Enter Coupon Code', + }, +}; + +export const InvalidState: Story = { + args: { + ...Default.args, + value: 'WW3vfAt', + isInvalid: true, + placeholderText: 'Enter Coupon Code', + }, +}; diff --git a/packages/ui/src/stories/molecules/DashboardWallet.stories.tsx b/packages/ui/src/stories/molecules/DashboardWallet.stories.tsx new file mode 100644 index 000000000..9633505d6 --- /dev/null +++ b/packages/ui/src/stories/molecules/DashboardWallet.stories.tsx @@ -0,0 +1,90 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { DashboardWallet } from '../../components'; + +const meta: Meta = { + component: DashboardWallet, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +const lang = { + dashboard: { + wallet: { + renewNow: 'Renew Now', + buyNow: 'Buy Now', + created: 'Created', + expiredOn: 'Expired on', + expiresIn: 'Expires in', + expiry: 'Expiry', + expiring: 'Expiring', + expired: 'Expired', + pending: 'Pending', + silver: 'Silver', + gold: 'Gold', + hours: 'Hours', + setupCover: 'Setup Cypherock Cover', + }, + }, +}; + +export const Default: Story = { + args: { + isNone: false, + type: 'silver', + isExpiring: false, + isExpired: false, + isPaymentPending: false, + name: 'My Default Wallet', + lang, + startDate: new Date('2024-01-21').getTime(), + expiryDate: new Date('2025-09-21').getTime(), + }, +}; + +export const SetupCover: Story = { + args: { + ...Default.args, + isNone: true, + }, +}; + +export const SilverPlan: Story = { + args: { + ...Default.args, + type: 'silver', + }, +}; + +export const GoldPlan: Story = { + args: { + ...Default.args, + type: 'gold', + name: 'My Gold Wallet', + }, +}; + +export const ExpiringPlan: Story = { + args: { + ...Default.args, + isExpiring: true, + }, +}; + +export const ExpiredPlan: Story = { + args: { + ...Default.args, + isExpired: true, + expiryDate: new Date('2023-01-21').getTime(), + }, +}; + +export const PendingPayment: Story = { + args: { + ...Default.args, + isPaymentPending: true, + }, +}; diff --git a/packages/ui/src/stories/molecules/DetailsCard.stories.tsx b/packages/ui/src/stories/molecules/DetailsCard.stories.tsx new file mode 100644 index 000000000..4c9a4ae33 --- /dev/null +++ b/packages/ui/src/stories/molecules/DetailsCard.stories.tsx @@ -0,0 +1,208 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { + ClockIcon, + EmailIconSmall, + EncryptedMessageIcon, + UserIcon, + WalletIconRounded, +} from '../../assets'; +import { + DetailsCard, + EditButton, + svgGradients, + Typography, +} from '../../components'; + +const meta: Meta = { + component: DetailsCard, + tags: ['autodocs'], + parameters: { actions: { argTypesRegex: null } }, +}; + +export default meta; + +type Story = StoryObj; + +const editButton = ( + alert('Edit Clicked')} /> +); + +const goldWalletIcon = ( + +); + +export const Default: Story = { + args: { + headerLeading: goldWalletIcon, + headerText: 'MyFunnyWallet', + headerTrailing: editButton, + $backgroundType: 'gold', + }, +}; + +export const HeaderOnly: Story = { + args: { + headerLeading: goldWalletIcon, + headerText: 'MyFunnyWallet', + headerTrailing: editButton, + headerOnly: true, + $backgroundType: 'gold', + }, +}; + +export const OwnerDetails: Story = { + args: { + headerText: 'Owner Details', + headerTrailing: editButton, + fields: [ + { + label: 'Name', + icon: UserIcon, + value: 'Alfred Bellows', + }, + { + label: 'Primary Email', + icon: EmailIconSmall, + value: 'doc.bellows@yahoo.com', + }, + { + label: 'Secondary Email', + icon: EmailIconSmall, + value: 'alfred@psych.com', + }, + { + label: 'Reminder Period', + icon: ClockIcon, + value: 'Every 3 Months', + trailing: editButton, + }, + ], + }, +}; + +export const WalletDetail: Story = { + args: { + headerLeading: goldWalletIcon, + headerText: 'MyNoNameWallet', + $backgroundType: 'gold', + headerTrailing: ( + + GOLD + + ), + fields: [ + { + label: 'Created on', + icon: ClockIcon, + value: '01 July 2024', + }, + { + label: 'Expiring on', + icon: ClockIcon, + value: '30 June 2024', + isDanger: true, + }, + { + label: 'Reminder Period', + icon: ClockIcon, + value: 'Every 3 Months', + trailing: editButton, + }, + ], + }, +}; + +export const WalletDetailSilver: Story = { + args: { + headerLeading: goldWalletIcon, + headerText: 'MyNoNameWallet', + $backgroundType: 'silver', + headerTrailing: ( + + SILVER + + ), + fields: [ + { + label: 'Created on', + icon: ClockIcon, + value: '01 July 2024', + }, + { + label: 'Expiring on', + icon: ClockIcon, + value: '30 June 2024', + isDanger: true, + }, + { + label: 'Reminder Period', + icon: ClockIcon, + value: 'Every 3 Months', + trailing: editButton, + }, + ], + }, +}; + +export const Nominee: Story = { + args: { + headerText: 'Nominee #1', + headerTrailing: editButton, + fields: [ + { + label: 'Name', + icon: UserIcon, + value: 'Alfred Bellows', + }, + { + label: 'Primary Email', + icon: EmailIconSmall, + value: 'doc.bellows@yahoo.com', + }, + { + label: 'Secondary Email', + icon: EmailIconSmall, + value: 'alfred@psych.com', + }, + ], + }, +}; + +export const NomineeWithEncryptedMessage: Story = { + args: { + headerText: 'Nominee #1', + headerTrailing: editButton, + fields: [ + { + label: 'Name', + icon: UserIcon, + value: 'Alfred Bellows', + }, + { + label: 'Primary Email', + icon: EmailIconSmall, + value: 'doc.bellows@yahoo.com', + }, + { + label: 'Secondary Email', + icon: EmailIconSmall, + value: 'alfred@psych.com', + }, + ], + footer: { + label: 'Encrypted Message', + icon: EncryptedMessageIcon, + trailing: editButton, + }, + }, +}; + +export const CardLocation: Story = { + args: { + headerText: 'Card Location', + headerTrailing: editButton, + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam eget dapibus est. Mauris varius sapien a diam elementum posuere. Maecenas aliquam nec justo a dictum. Aliquam eu condimentum mi, eu vulputate ipsum. Proin vel semper nisl. Donec ultricies consectetur dapibus. Donec suscipit, mi sed tristique feugiat, urna ipsum viverra risus, vitae commodo tortor est interdum ligula. Fusce tellus mi, malesuada tristique mauris a, pulvinar varius metus. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec in nulla sit amet ex cursus dictum. Nam felis odio, egestas sed porttitor eu, consequat eget dolor. Phasellus luctus, arcu non auctor euismod, lectus quam tempus lacus, in sollicitudin elit risus ut ex.', + }, +}; diff --git a/packages/ui/src/stories/atoms/Input.stories.tsx b/packages/ui/src/stories/molecules/Input.stories.tsx similarity index 100% rename from packages/ui/src/stories/atoms/Input.stories.tsx rename to packages/ui/src/stories/molecules/Input.stories.tsx diff --git a/packages/ui/src/stories/molecules/ManyInMany.stories.tsx b/packages/ui/src/stories/molecules/ManyInMany.stories.tsx new file mode 100644 index 000000000..11c94c49f --- /dev/null +++ b/packages/ui/src/stories/molecules/ManyInMany.stories.tsx @@ -0,0 +1,33 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { ManyInMany } from '../../components'; + +const meta: Meta = { + component: ManyInMany, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + title: 'DDDDDDDDDDDD', + disabled: false, + }, +}; + +export const Disabled: Story = { + args: { + title: 'DDDDDDDDDDDD', + disabled: true, + }, +}; + +export const Active: Story = { + args: { + title: 'DDDDDDDDDDDD', + isActive: true, + }, +}; diff --git a/packages/ui/src/stories/molecules/OTPInput.stories.tsx b/packages/ui/src/stories/molecules/OTPInput.stories.tsx new file mode 100644 index 000000000..e9981291a --- /dev/null +++ b/packages/ui/src/stories/molecules/OTPInput.stories.tsx @@ -0,0 +1,102 @@ +/* eslint-disable no-template-curly-in-string */ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { OTPInput } from '../../components/molecules'; + +const meta: Meta = { + component: OTPInput, + render: args => { + const [value, onChange] = React.useState(''); + + return ; + }, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Idle: Story = { + args: { + title: 'Enter OTP', + status: 'idle', + otpLength: 6, + actionText: 'Resend OTP', + subText: '4 tries remaining 25..', + infoText: [ + 'An email has been sent to **${email}**', + 'Please check your email for the OTP.', + ], + textVariables: { + email: 'john@gmail.com', + }, + onAction: () => { + console.log('Action'); + }, + }, +}; + +export const Error: Story = { + args: { + title: 'Wrong OTP', + status: 'error', + otpLength: 6, + actionText: 'Resend OTP', + subText: '3 tries remaining 25..', + infoText: [ + 'An email has been sent to **${email}**', + 'Please check your email for the OTP.', + ], + textVariables: { + email: 'john@gmail.com', + }, + onAction: () => { + console.log('Action'); + }, + }, +}; + +export const Success: Story = { + args: { + title: 'Please wait while we redirect you...', + status: 'success', + otpLength: 6, + actionText: 'Resend OTP', + disabled: true, + subText: '3 tries remaining 25..', + infoText: [ + 'An email has been sent to **${email}**', + 'Please check your email for the OTP.', + ], + textVariables: { + email: 'john@gmail.com', + }, + onAction: () => { + console.log('Action'); + }, + }, +}; + +export const RetryExceeded: Story = { + args: { + title: 'You have 0 retries remaining', + status: 'retryExceeded', + errorSubText: 'Please exit the flow and restart', + otpLength: 6, + actionText: 'Resend OTP', + disabled: true, + subText: '3 tries remaining 25..', + infoText: [ + 'An email has been sent to **${email}**', + 'Please check your email for the OTP.', + ], + textVariables: { + email: 'john@gmail.com', + }, + onAction: () => { + console.log('Action'); + }, + }, +}; diff --git a/packages/ui/src/stories/molecules/OneInMany.stories.tsx b/packages/ui/src/stories/molecules/OneInMany.stories.tsx new file mode 100644 index 000000000..2be49d653 --- /dev/null +++ b/packages/ui/src/stories/molecules/OneInMany.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { OneInMany } from '../../components'; + +const meta: Meta = { + component: OneInMany, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const StyleType1: Story = { + args: { + title: 'YES', + description: 'Description goes here', + $styleType: '1', + }, +}; + +export const StyleType2: Story = { + args: { + title: 'NO', + description: 'Different description', + $styleType: '2', + }, +}; diff --git a/packages/ui/src/stories/molecules/Payment.stories.tsx b/packages/ui/src/stories/molecules/Payment.stories.tsx new file mode 100644 index 000000000..c27b3b499 --- /dev/null +++ b/packages/ui/src/stories/molecules/Payment.stories.tsx @@ -0,0 +1,93 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +import { Payment } from '../../components'; + +const meta: Meta = { + component: Payment, + tags: ['autodocs'], + render: args => { + const [value, setValue] = React.useState(''); + const [applied, setApplied] = React.useState(args.applied || false); + + const onChange = (_value: string) => { + setValue(_value); + }; + + const onApply = () => { + if (value.trim() !== '') { + setApplied(true); + } + }; + + const onDelete = () => { + setValue(''); + setApplied(false); + }; + + return ( + + ); + }, +}; + +export default meta; + +type Story = StoryObj; + +const lang = { + heading: 'Buy plan on the Cypherock website', + form: { + promoField: { + label: 'Promo code / coupon', + placeholder: 'Enter code', + }, + }, + couponInput: { + appliedButtonText: 'Coupon applied:', + applyButtonText: 'Apply', + }, + noOfYear: 'Number of years', + externalLink: 'some link', + total: 'Total', + year: 'year', + error: { + heading: 'Invalid Coupon Code', + subtext: 'Please try a different coupon code', + }, +}; + +export const Default: Story = { + args: { + lang, + applied: false, + onApply: () => { + // Dummy functions + }, + onDelete: () => { + // Dummy functions + }, + }, +}; + +export const Applied: Story = { + args: { + ...Default.args, + applied: true, + year: 2, + }, +}; + +export const Error: Story = { + args: { + ...Default.args, + error: lang.error, + }, +}; diff --git a/packages/ui/src/stories/molecules/PlanCard.stories.tsx b/packages/ui/src/stories/molecules/PlanCard.stories.tsx new file mode 100644 index 000000000..e4bb52a40 --- /dev/null +++ b/packages/ui/src/stories/molecules/PlanCard.stories.tsx @@ -0,0 +1,47 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { PlanCard } from '../../components'; + +const meta: Meta = { + component: PlanCard, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const SilverPlan: Story = { + args: { + type: 'silver', + heading: 'Silver', + description: + 'Secure your wallet PIN with us and easily recover it if forgotten, ensuring continuous access to your crypto assets.', + features: [ + { text: 'No KYC required', available: true }, + { text: 'Multi-Chain', available: true }, + { text: 'PIN Recovery', available: true }, + { text: 'Estate Recovery', available: false }, + { text: 'Passphrase Recovery', available: false }, + ], + buttonText: 'SELECT', + }, +}; + +export const GoldPlan: Story = { + args: { + type: 'gold', + heading: 'Gold', + description: + 'Ensure your crypto assets are inherited by loved ones effortlessly, bypassing complex legal restrictions for smooth transfers.', + features: [ + { text: 'No KYC required', available: true }, + { text: 'Multi-Chain', available: true }, + { text: 'PIN Recovery', available: true }, + { text: 'Estate Recovery', available: true }, + { text: 'Passphrase Recovery', available: true }, + ], + buttonText: 'SELECT', + popularTagText: 'MOST POPULAR', + }, +}; diff --git a/packages/ui/src/stories/molecules/Reminder.stories.tsx b/packages/ui/src/stories/molecules/Reminder.stories.tsx new file mode 100644 index 000000000..f34a19425 --- /dev/null +++ b/packages/ui/src/stories/molecules/Reminder.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Reminder } from '../../components'; + +const meta: Meta = { + component: Reminder, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + date: '3 Months', + disabled: false, + }, +}; + +export const Disabled: Story = { + args: { + date: '3 Months', + disabled: true, + }, +}; diff --git a/packages/ui/src/stories/molecules/TextAreaInput.stories.tsx b/packages/ui/src/stories/molecules/TextAreaInput.stories.tsx new file mode 100644 index 000000000..0c8bbcf82 --- /dev/null +++ b/packages/ui/src/stories/molecules/TextAreaInput.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { TextAreaInput } from '../../components'; + +const meta: Meta = { + component: TextAreaInput, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + placeholder: 'Enter your message', + }, +}; + +export const MaxChars: Story = { + args: { + placeholder: 'Enter your message', + maxChars: 100, + currentChars: 10, + }, +}; diff --git a/packages/ui/src/themes/color.styled.ts b/packages/ui/src/themes/color.styled.ts index 9ae764c25..0578709a0 100644 --- a/packages/ui/src/themes/color.styled.ts +++ b/packages/ui/src/themes/color.styled.ts @@ -17,6 +17,32 @@ export const colors = { silver: 'linear-gradient(180deg, #A2ADB3 -2.08%, #F3F1F2 34.27%, #BCC3C9 66.28%, #DCDFE4 102.08%)', stripe: 'linear-gradient(90deg, #211C18 1.69%, #242018 100%)', + goldenhint: + 'linear-gradient(263deg, rgba(139, 100, 41, 0.14) 5.24%, rgba(38, 34, 31, 0.00) 55.22%), #272320', + silverhint: + 'linear-gradient(263deg, rgba(194, 194, 194, 0.14) 5.24%, rgba(38, 34, 31, 0.00) 55.22%), #272320', + conicGradient: { + default: + 'conic-gradient(from 0deg, transparent,90deg, transparent, 90deg, #E9B873 ,180deg, #FEDD8F, 270deg, #B78D51, 360deg ,transparent, 360deg, transparent)', + secondary: + 'conic-gradient(from 0deg, #A2ADB3 ,33deg, #F3F1F2, 67deg, #BCC3C9, 101deg, #DCDFE4,135deg ,transparent, 135deg, transparent)', + expirig: + 'conic-gradient(from 0deg, #FF624C ,270deg, #FF624C,270deg ,transparent, 270deg, transparent)', + golden: + 'conic-gradient(from 0deg, #E9B873 ,30deg, #FEDD8F, 60deg, #B78D51, 90deg ,transparent, 90deg, transparent)', + silver: + 'conic-gradient(from 0deg, transparent,135deg, transparent, 135deg, #A2ADB3 ,191deg, #F3F1F2, 247deg, #BCC3C9, 304deg, #DCDFE4, 360deg ,transparent, 360deg, transparent)', + notExpiring: + 'conic-gradient(from 0deg, transparent, 270deg, transparent, 270deg, #E9B873 ,300deg, #FEDD8F, 330deg, #B78D51, 360deg ,transparent, 360deg, transparent)', + }, + cardDefault: + 'linear-gradient(300deg, rgba(96, 58, 23, 0.20) 0%, rgba(0, 0, 0, 0.00) 57.81%, rgba(0, 0, 0, 0.00) 100%), #2A2827', + cardSelected: + 'linear-gradient(285deg, rgba(96, 58, 23, 0.20) 0%, rgba(0, 0, 0, 0.00) 60.65%), #2A2827', + cardHover: + 'linear-gradient(105deg, rgba(96, 58, 23, 0.20) 0%, rgba(0, 0, 0, 0.00) 60.65%), #332F2D', + title: `linear-gradient(90deg,#e9b873 0.19%,#fedd8f 37.17%,#b78d51 100.19%)`, + plan: 'linear-gradient(90deg, rgba(224, 187, 117, 0.10) 0%, rgba(39, 35, 32, 0.00) 100%), #272320', }, info: { main: '#F1AE4A', @@ -51,6 +77,16 @@ export const colors = { normal: '#CCC4BE', divider: '#333130', dialog: '#2B2420', + separator: '#39322C', + greenStroke: '#00FF75', + redStroke: '#FF0202', + }, + boxShadow: { + selected: '#1B1813', + timer: { + main: '#2e2523', + text: '#242322', + }, }, background: { progressBar: '#1F1915', @@ -82,8 +118,26 @@ export const colors = { filterItem: `#1F1C19`, calendar: '#342F2C', calendarHeader: '#211C18', + timer: { + main: '#3e3a38', + default: '#261f17', + silver: '#26221e', + expiring: '#271a15', + secondary: '#2a2827', + }, + silver: '#a2adb3', + slate: '#312d2a', + cardDisabled: '#282522', + cardSelected: '#2A2827', + slateDark: '#423F3C', + slateLight: '#302C29', + headlineLight: '#655F53', + cardActive: '#262423', + videoError: '#14110f', + featureBanner: '#282828', }, border: { + darkSlate: '#030303', popup: '#2C2520', list: '#2C2824', input: '#3C3937', @@ -108,6 +162,8 @@ export const colors = { subMenuLeft: '#534B44', topbar: '#342C26', card: '#534A44', + selected: '#e0bb74', + success: '#51C61A', }, shadow: { dropdown: '#0f0d0b', diff --git a/packages/ui/src/themes/theme.styled.ts b/packages/ui/src/themes/theme.styled.ts index addd75215..56f0d5efa 100644 --- a/packages/ui/src/themes/theme.styled.ts +++ b/packages/ui/src/themes/theme.styled.ts @@ -46,6 +46,9 @@ export const theme = { muted: { main: colors.text.muted, }, + error: { + main: colors.text.error, + }, golden: colors.gradients.golden, silver: colors.gradients.silver, highlight: colors.gradients.highlight, @@ -58,6 +61,13 @@ export const theme = { }, shadow: { dropdown: colors.shadow.dropdown, + selected: colors.boxShadow.selected, + }, + gradients: { + cardDefault: colors.gradients.cardDefault, + cardSelected: colors.gradients.cardSelected, + cardHover: colors.gradients.cardHover, + title: colors.gradients.title, }, background: { progress: colors.background.progressBar, @@ -74,6 +84,9 @@ export const theme = { disabled: colors.disabled.background, bar: colors.background.bar, gold: colors.gradients.golden, + goldenhint: colors.gradients.goldenhint, + silverhint: colors.gradients.silverhint, + plan: colors.gradients.plan, golden: colors.background.gold, success: colors.success.main, successSecondary: colors.success.secondary, @@ -93,12 +106,39 @@ export const theme = { message: colors.background.message, breadcrumbSeparator: colors.background.breadcrumbSeparator, batchTransactionBody: colors.background.batchTransactionBody, + slateLight: colors.background.slateLight, + slate: colors.background.slate, filterItem: colors.background.filterItem, calendar: colors.background.calendar, calendarHeader: colors.background.calendarHeader, popup: colors.border.popup, danger: colors.background.danger, error: colors.background.error, + silver: colors.background.silver, + cardDisabled: colors.background.cardDisabled, + cardSelected: colors.background.cardSelected, + slateDark: colors.background.slateDark, + cardActive: colors.background.cardActive, + videoError: colors.background.videoError, + featureBanner: colors.background.featureBanner, + timer: { + main: colors.background.timer.main, + default: colors.background.timer.default, + silver: colors.background.timer.silver, + expiring: colors.background.timer.expiring, + secondary: colors.background.timer.secondary, + }, + }, + boxShadow: { + timer: { + main: colors.boxShadow.timer.main, + text: colors.boxShadow.timer.text, + }, + slate: colors.background.slate, + cardSelected: colors.background.cardSelected, + cardDisabled: colors.background.cardDisabled, + slateDark: colors.background.slateDark, + headlineLight: colors.background.headlineLight, }, border: { popup: colors.border.popup, @@ -112,6 +152,7 @@ export const theme = { danger: colors.border.danger, bar: colors.border.bar, white: colors.border.white, + darkSlate: colors.border.darkSlate, table: { title: colors.border.table.title, row: colors.border.table.row, @@ -126,6 +167,8 @@ export const theme = { subMenuLeft: colors.border.subMenuLeft, topbar: colors.border.topbar, card: colors.border.card, + selected: colors.border.selected, + success: colors.border.success, }, contrastThreshold: 3, tonalOffset: 0.2, diff --git a/packages/utils/src/datetime.ts b/packages/utils/src/datetime.ts index 5c6567236..f5a0d9f03 100644 --- a/packages/utils/src/datetime.ts +++ b/packages/utils/src/datetime.ts @@ -3,3 +3,12 @@ export const formatDateToUTCString = (date: number) => { return `${isoDate.substring(0, 10)} ${isoDate.substring(11, 19)}`; }; + +export const formatSecondsToMinutes = (seconds: number) => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + + return `${minutes.toString().padStart(2, '0')}:${remainingSeconds + .toString() + .padStart(2, '0')}`; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 93b5a2202..9bf6e3f2c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -387,7 +387,7 @@ importers: version: 4.9.5 vite: specifier: ^4.3.9 - version: 4.3.9(@types/node@18.15.11)(less@4.2.0) + version: 4.3.9(@types/node@20.5.1) vite-plugin-electron: specifier: ^0.12.0 version: 0.12.0 @@ -398,6 +398,82 @@ importers: specifier: ^0.2.0 version: 0.2.0 + packages/app-support-inheritance: + dependencies: + '@cypherock/cysync-interfaces': + specifier: workspace:^ + version: link:../interfaces + '@cypherock/cysync-utils': + specifier: workspace:^ + version: link:../utils + '@cypherock/sdk-interfaces': + specifier: ^0.0.15 + version: 0.0.15 + '@cypherock/sdk-utils': + specifier: ^0.0.18 + version: 0.0.18 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + devDependencies: + '@cypherock/eslint-config': + specifier: workspace:^ + version: link:../util-eslint-config + '@cypherock/jest-config': + specifier: workspace:^ + version: link:../util-jest-config + '@cypherock/prettier-config': + specifier: workspace:^ + version: link:../util-prettier-config + '@cypherock/tsconfig': + specifier: workspace:^ + version: link:../util-tsconfig + '@jest/globals': + specifier: ^29.5.0 + version: 29.5.0 + '@stryker-mutator/core': + specifier: ^7.0.2 + version: 7.0.2 + '@stryker-mutator/jest-runner': + specifier: ^7.0.2 + version: 7.0.2(@stryker-mutator/core@7.0.2) + '@stryker-mutator/typescript-checker': + specifier: ^7.0.2 + version: 7.0.2(@stryker-mutator/core@7.0.2)(typescript@4.9.5) + '@types/jest': + specifier: ^29.5.2 + version: 29.5.2 + '@types/lodash': + specifier: ^4.14.195 + version: 4.14.195 + '@types/node': + specifier: 18.15.11 + version: 18.15.11 + eslint: + specifier: ^8.43.0 + version: 8.43.0 + jest: + specifier: ^29.5.0 + version: 29.5.0(@types/node@18.15.11)(ts-node@10.9.1) + lint-staged: + specifier: ^13.2.2 + version: 13.2.2 + prettier: + specifier: ^2.8.8 + version: 2.8.8 + rimraf: + specifier: ^5.0.1 + version: 5.0.1 + ts-jest: + specifier: ^29.1.0 + version: 29.1.0(@babel/core@7.24.7)(esbuild@0.18.20)(jest@29.5.0)(typescript@4.9.5) + typescript: + specifier: ^4.9.5 + version: 4.9.5 + packages/automation-scripts: dependencies: '@cypherock/coins': @@ -1467,6 +1543,9 @@ importers: '@aws-crypto/sha256-browser': specifier: ^4.0.0 version: 4.0.0 + '@cypherock/app-support-inheritance': + specifier: workspace:^ + version: link:../app-support-inheritance '@cypherock/coin-support': specifier: workspace:^ version: link:../coin-support @@ -1689,7 +1768,7 @@ importers: version: 8.43.0 eslint-import-resolver-typescript: specifier: ^3.5.5 - version: 3.5.5(@typescript-eslint/parser@5.59.11)(eslint-plugin-import@2.27.5)(eslint@8.43.0) + version: 3.5.5(eslint-plugin-import@2.27.5)(eslint@8.43.0) ethers: specifier: ^6.7.0 version: 6.7.0 @@ -1874,7 +1953,7 @@ importers: version: 8.43.0 eslint-import-resolver-typescript: specifier: ^3.5.5 - version: 3.5.5(@typescript-eslint/parser@5.59.11)(eslint-plugin-import@2.27.5)(eslint@8.43.0) + version: 3.5.5(eslint-plugin-import@2.27.5)(eslint@8.43.0) jest: specifier: ^29.5.0 version: 29.5.0(@types/node@18.15.11)(ts-node@10.9.1) @@ -2178,15 +2257,18 @@ importers: focus-trap-react: specifier: ^10.2.3 version: 10.2.3(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0) - marked-react: - specifier: ^2.0.0 - version: 2.0.0(react@18.2.0) + marked: + specifier: ^13.0.2 + version: 13.0.2 rc-tooltip: specifier: ^6.1.2 version: 6.1.2(react-dom@18.2.0)(react@18.2.0) react: specifier: ^18.2.0 version: 18.2.0 + react-circular-progressbar: + specifier: ^2.1.0 + version: 2.1.0(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -2196,6 +2278,9 @@ importers: react-multi-date-picker: specifier: ^4.1.2 version: 4.1.2(react-dom@18.2.0)(react@18.2.0) + react-otp-input: + specifier: ^3.1.1 + version: 3.1.1(react-dom@18.2.0)(react@18.2.0) react-tsparticles: specifier: ^2.10.1 version: 2.10.1(react@18.2.0) @@ -2236,9 +2321,6 @@ importers: '@storybook/addon-links': specifier: 7.2.0 version: 7.2.0(react-dom@18.2.0)(react@18.2.0) - '@storybook/addon-onboarding': - specifier: 1.0.8 - version: 1.0.8(react-dom@18.2.0)(react@18.2.0) '@storybook/addon-styling': specifier: ^1.3.5 version: 1.3.5(@types/react-dom@18.2.6)(@types/react@18.2.13)(less@4.2.0)(postcss@8.4.38)(react-dom@18.2.0)(react@18.2.0)(typescript@4.9.5)(webpack@5.92.1) @@ -5717,7 +5799,7 @@ packages: '@cypherock/sdk-interfaces': 0.0.15 compare-versions: 6.0.0-rc.1 protobufjs: 7.3.2 - semver: 7.5.3 + semver: 7.6.2 uuid: 9.0.0 dev: false @@ -9906,21 +9988,6 @@ packages: - '@types/react-dom' dev: true - /@storybook/addon-onboarding@1.0.8(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-3WwCocUuTibHWO89uzAbeONEn6x9NkmxA0mOU4rYPhEsMQ4NDNDD1GXcievZmpwt5VviFgw5FRx8+BxPn54YmQ==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - '@storybook/telemetry': 7.6.20 - react: 18.2.0 - react-confetti: 6.1.0(react@18.2.0) - react-dom: 18.2.0(react@18.2.0) - transitivePeerDependencies: - - encoding - - supports-color - dev: true - /@storybook/addon-outline@7.2.0(@types/react-dom@18.2.6)(@types/react@18.2.13)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-pP9Umngd8OsmVYAhRQqY4HGgTeQK63tZeNDOSdB5Jab4f6wDrdHUn6OMo0Hsfzw65g7mSQmEexnxbq1DrgKU1Q==} peerDependencies: @@ -10642,22 +10709,6 @@ packages: - supports-color dev: false - /@storybook/csf-tools@7.6.20: - resolution: {integrity: sha512-rwcwzCsAYh/m/WYcxBiEtLpIW5OH1ingxNdF/rK9mtGWhJxXRDV8acPkFrF8rtFWIVKoOCXu5USJYmc3f2gdYQ==} - dependencies: - '@babel/generator': 7.24.7 - '@babel/parser': 7.24.7 - '@babel/traverse': 7.24.7 - '@babel/types': 7.24.7 - '@storybook/csf': 0.1.9 - '@storybook/types': 7.6.20 - fs-extra: 11.2.0 - recast: 0.23.9 - ts-dedent: 2.2.0 - transitivePeerDependencies: - - supports-color - dev: true - /@storybook/csf@0.0.1: resolution: {integrity: sha512-USTLkZze5gkel8MYCujSRBVIrUQ3YPBrLOx7GNk/0wttvVtlzWXAq9eLbQ4p/NicGxP+3T7KPEMVV//g+yubpw==} dependencies: @@ -11081,22 +11132,6 @@ packages: - supports-color dev: true - /@storybook/telemetry@7.6.20: - resolution: {integrity: sha512-dmAOCWmOscYN6aMbhCMmszQjoycg7tUPRVy2kTaWg6qX10wtMrvEtBV29W4eMvqdsoRj5kcvoNbzRdYcWBUOHQ==} - dependencies: - '@storybook/client-logger': 7.6.20 - '@storybook/core-common': 7.6.20 - '@storybook/csf-tools': 7.6.20 - chalk: 4.1.2 - detect-package-manager: 2.0.1 - fetch-retry: 5.0.6 - fs-extra: 11.2.0 - read-pkg-up: 7.0.1 - transitivePeerDependencies: - - encoding - - supports-color - dev: true - /@storybook/testing-library@0.2.0: resolution: {integrity: sha512-Ff6jNnrsosmDshgCf0Eb5Cz7IA34p/1Ps5N3Kp3598kfXpBSccSkQQvVFUXC3kIHw/isIXWPqntZuKqnWUz7Gw==} dependencies: @@ -12407,7 +12442,7 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.24.7(@babel/core@7.22.5) '@babel/plugin-transform-react-jsx-source': 7.24.7(@babel/core@7.22.5) react-refresh: 0.14.2 - vite: 4.3.9(@types/node@18.15.11)(less@4.2.0) + vite: 4.3.9(@types/node@20.5.1) transitivePeerDependencies: - supports-color dev: true @@ -13675,7 +13710,7 @@ packages: '@babel/core': 7.22.5 find-cache-dir: 3.3.2 schema-utils: 4.2.0 - webpack: 5.92.1(esbuild@0.18.20) + webpack: 5.92.1 dev: true /babel-plugin-istanbul@6.1.1: @@ -15394,7 +15429,7 @@ packages: postcss-modules-values: 4.0.0(postcss@8.4.38) postcss-value-parser: 4.2.0 semver: 7.5.3 - webpack: 5.92.1(esbuild@0.18.20) + webpack: 5.92.1 dev: true /css-select@4.3.0: @@ -16800,6 +16835,30 @@ packages: - supports-color dev: true + /eslint-import-resolver-typescript@3.5.5(eslint-plugin-import@2.27.5)(eslint@8.43.0): + resolution: {integrity: sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + dependencies: + debug: 4.3.5(supports-color@8.1.1) + enhanced-resolve: 5.17.0 + eslint: 8.43.0 + eslint-module-utils: 2.8.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.43.0) + eslint-plugin-import: 2.27.5(eslint-import-resolver-typescript@3.5.5)(eslint@8.43.0) + get-tsconfig: 4.7.5 + globby: 13.2.2 + is-core-module: 2.14.0 + is-glob: 4.0.3 + synckit: 0.8.8 + transitivePeerDependencies: + - '@typescript-eslint/parser' + - eslint-import-resolver-node + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-module-utils@2.8.1(@typescript-eslint/parser@5.59.11)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.43.0): resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} engines: {node: '>=4'} @@ -16830,6 +16889,35 @@ packages: - supports-color dev: true + /eslint-module-utils@2.8.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.43.0): + resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + dependencies: + debug: 3.2.7 + eslint: 8.43.0 + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.5.5(eslint-plugin-import@2.27.5)(eslint@8.43.0) + transitivePeerDependencies: + - supports-color + dev: true + /eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.59.11)(eslint-import-resolver-typescript@3.5.5)(eslint@8.43.0): resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} engines: {node: '>=4'} @@ -16863,6 +16951,38 @@ packages: - supports-color dev: true + /eslint-plugin-import@2.27.5(eslint-import-resolver-typescript@3.5.5)(eslint@8.43.0): + resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + dependencies: + array-includes: 3.1.8 + array.prototype.flat: 1.3.2 + array.prototype.flatmap: 1.3.2 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 8.43.0 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.8.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.5.5)(eslint@8.43.0) + has: 1.0.4 + is-core-module: 2.14.0 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.values: 1.2.0 + resolve: 1.22.8 + semver: 6.3.1 + tsconfig-paths: 3.15.0 + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + dev: true + /eslint-plugin-jsx-a11y@6.7.1(eslint@8.43.0): resolution: {integrity: sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==} engines: {node: '>=4.0'} @@ -19613,7 +19733,7 @@ packages: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1(@types/node@18.15.11)(typescript@4.9.5) + ts-node: 10.9.1(@types/node@20.5.1)(typescript@4.9.5) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -21089,18 +21209,9 @@ packages: dependencies: react: 18.2.0 - /marked-react@2.0.0(react@18.2.0): - resolution: {integrity: sha512-Mp5HqfONf/RDqFtA+6xw2EjKkSbA8/xNPwyJ8ewLy/q3v21lRsPA7h+HUndVAW/yEIoebvcyzzSDpbjzL/xjZg==} - peerDependencies: - react: ^16.8.0 || >=17.0.0 - dependencies: - marked: 6.0.0 - react: 18.2.0 - dev: false - - /marked@6.0.0: - resolution: {integrity: sha512-7E3m/xIlymrFL5gWswIT4CheIE3fDeh51NV09M4x8iOc7NDYlyERcQMLAIHcSlrvwliwbPQ4OGD+MpPSYiQcqw==} - engines: {node: '>= 16'} + /marked@13.0.2: + resolution: {integrity: sha512-J6CPjP8pS5sgrRqxVRvkCIkZ6MFdRIjDkwUwgJ9nL2fbmM6qGQeB2C16hi8Cc9BOzj6xXzy0jyi0iPIfnMHYzA==} + engines: {node: '>= 18'} hasBin: true dev: false @@ -21864,7 +21975,7 @@ packages: webpack: ^5.0.0 dependencies: loader-utils: 2.0.4 - webpack: 5.92.1(esbuild@0.18.20) + webpack: 5.92.1 dev: true /node-preload@0.2.1: @@ -22639,9 +22750,6 @@ packages: resolution: {integrity: sha512-2GTVocFkwblV/TIg9AmT7TI2fO4xdWkyN8aFUEVtiVNWt96GTR3FgQyHFValfCbcj1k9Xf962Ws2hYXYUr9k1Q==} engines: {node: '>= 12.0.0'} hasBin: true - peerDependenciesMeta: - '@parcel/core': - optional: true dependencies: '@parcel/config-default': 2.9.3(@parcel/core@2.9.3)(typescript@4.9.5) '@parcel/core': 2.9.3 @@ -23539,6 +23647,14 @@ packages: pure-color: 1.3.0 dev: false + /react-circular-progressbar@2.1.0(react@18.2.0): + resolution: {integrity: sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /react-colorful@5.6.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} peerDependencies: @@ -23548,16 +23664,6 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - /react-confetti@6.1.0(react@18.2.0): - resolution: {integrity: sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw==} - engines: {node: '>=10.18'} - peerDependencies: - react: ^16.3.0 || ^17.0.1 || ^18.0.0 - dependencies: - react: 18.2.0 - tween-functions: 1.2.0 - dev: true - /react-date-object@2.1.8: resolution: {integrity: sha512-94zg/9r29v/VsKsJT0E5+tPg85IyMirkU/B7qBWlhT7RDSuf9x5dczfbQOsj9TbzSVUK/Vx/ZDboOBy9hA381w==} dev: false @@ -23684,6 +23790,16 @@ packages: react-element-popper: 2.1.6(react-dom@18.2.0)(react@18.2.0) dev: false + /react-otp-input@3.1.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bjPavgJ0/Zmf/AYi4onj8FbH93IjeD+e8pWwxIJreDEWsU1ILR5fs8jEJmMGWSBe/yyvPP6X/W6Mk9UkOCkTPw==} + peerDependencies: + react: '>=16.8.6 || ^17.0.0 || ^18.0.0' + react-dom: '>=16.8.6 || ^17.0.0 || ^18.0.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-player@2.14.1(react@18.2.0): resolution: {integrity: sha512-jILj7F9o+6NHzrJ1GqZIxfJgskvGmKeJ05FNhPvgiCpvMZFmFneKEkukywHcULDO2lqITm+zcEkLSq42mX0FbA==} peerDependencies: @@ -25395,7 +25511,7 @@ packages: peerDependencies: webpack: ^5.0.0 dependencies: - webpack: 5.92.1(esbuild@0.18.20) + webpack: 5.92.1 dev: true /styled-components@6.0.0-rc.1(react-dom@18.2.0)(react@18.2.0): @@ -25678,6 +25794,30 @@ packages: webpack: 5.92.1(esbuild@0.18.20) dev: true + /terser-webpack-plugin@5.3.10(webpack@5.92.1): + resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@swc/core': '*' + esbuild: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@swc/core': + optional: true + esbuild: + optional: true + uglify-js: + optional: true + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + jest-worker: 27.5.1 + schema-utils: 3.3.0 + serialize-javascript: 6.0.2 + terser: 5.31.1 + webpack: 5.92.1 + dev: true + /terser@4.8.1: resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==} engines: {node: '>=6.0.0'} @@ -26490,10 +26630,6 @@ packages: turbo-windows-arm64: 1.10.6 dev: true - /tween-functions@1.2.0: - resolution: {integrity: sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==} - dev: true - /tweetnacl@1.0.3: resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} @@ -27230,6 +27366,39 @@ packages: fsevents: 2.3.3 dev: true + /vite@4.3.9(@types/node@20.5.1): + resolution: {integrity: sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.5.1 + esbuild: 0.17.19 + postcss: 8.4.38 + rollup: 3.29.4 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} dev: true @@ -27576,6 +27745,46 @@ packages: /webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + /webpack@5.92.1: + resolution: {integrity: sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.5 + '@webassemblyjs/ast': 1.12.1 + '@webassemblyjs/wasm-edit': 1.12.1 + '@webassemblyjs/wasm-parser': 1.12.1 + acorn: 8.12.0 + acorn-import-attributes: 1.9.5(acorn@8.12.0) + browserslist: 4.23.1 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.17.0 + es-module-lexer: 1.5.4 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + json-parse-even-better-errors: 2.3.1 + loader-runner: 4.3.0 + mime-types: 2.1.35 + neo-async: 2.6.2 + schema-utils: 3.3.0 + tapable: 2.2.1 + terser-webpack-plugin: 5.3.10(webpack@5.92.1) + watchpack: 2.4.1 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - '@swc/core' + - esbuild + - uglify-js + dev: true + /webpack@5.92.1(esbuild@0.18.20): resolution: {integrity: sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==} engines: {node: '>=10.13.0'} diff --git a/scripts/clean.js b/scripts/clean.js index 4cf11ae2d..d9606abca 100644 --- a/scripts/clean.js +++ b/scripts/clean.js @@ -16,6 +16,7 @@ const packages = { ], 'apps/cli': [...commonFolders, 'release'], 'packages/coin-support': [...commonFolders], + 'packages/app-support-inheritance': [...commonFolders], 'packages/coin-support-evm': [...commonFolders], 'packages/coin-support-btc': [...commonFolders], 'packages/coin-support-solana': [...commonFolders], diff --git a/scripts/inheritanceSetup.js b/scripts/inheritanceSetup.js new file mode 100644 index 000000000..93acdc324 --- /dev/null +++ b/scripts/inheritanceSetup.js @@ -0,0 +1,49 @@ +const { execSync } = require('child_process'); +const path = require('path'); +const fs = require('fs/promises'); + +const rootPath = path.join(__dirname, '..'); + +const inheritanceAppPath = path.join( + __dirname, + '..', + 'packages', + 'app-support-inheritance', +); + +const sdkPath = path.join(__dirname, '..', 'submodules', 'sdk'); + +const run = async () => { + const args = process.argv.slice(2); + const packageJson = JSON.parse( + await fs.readFile(path.join(inheritanceAppPath, 'package.json'), 'utf8'), + ); + + const origionalPackageJson = structuredClone(packageJson); + delete packageJson.dependencies['@cypherock/sdk-app-inheritance']; + + await fs.writeFile( + path.join(inheritanceAppPath, 'package.json'), + JSON.stringify(packageJson, null, 2), + ); + + console.log(`Running "pnpm install ${args.join(' ')}" on app...`); + execSync(`pnpm install ${args.join(' ')}`, { + cwd: rootPath, + stdio: 'inherit', + }); + + await fs.writeFile( + path.join(inheritanceAppPath, 'package.json'), + JSON.stringify(origionalPackageJson, null, 2), + ); + + console.log('Running pnpm install on SDK...'); + execSync('pnpm install', { cwd: sdkPath, stdio: 'inherit' }); + + console.log('Building SDK...'); + execSync('pnpm build', { cwd: sdkPath, stdio: 'inherit' }); + execSync('pnpm build:submodules', { cwd: rootPath, stdio: 'inherit' }); +}; + +run(); diff --git a/submodules/sdk b/submodules/sdk index 117bfba4d..2c7c4b455 160000 --- a/submodules/sdk +++ b/submodules/sdk @@ -1 +1 @@ -Subproject commit 117bfba4d128db428415bacf75c710a96e2ebdd5 +Subproject commit 2c7c4b45536649f93b19058987e67fd594c8aaae