diff --git a/apps/mobile/app/components/dialog/functions.ts b/apps/mobile/app/components/dialog/functions.ts index 4798c5986..25d643895 100644 --- a/apps/mobile/app/components/dialog/functions.ts +++ b/apps/mobile/app/components/dialog/functions.ts @@ -17,10 +17,12 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { KeyboardTypeOptions } from "react-native"; +import { KeyboardTypeOptions, TextInput } from "react-native"; import { eSendEvent } from "../../services/event-manager"; import { eCloseSimpleDialog, eOpenSimpleDialog } from "../../utils/events"; import { ButtonProps } from "../ui/button"; +import { FieldValidator, FormRef } from "../ui/input/form-input"; +import { RefObject } from "react"; export type DialogInfo = { title?: string; @@ -43,6 +45,18 @@ export type DialogInfo = { | "errorShade"; icon?: string; paragraphColor: string; + form?: { + formRef: FormRef; + items: { + name: string; + placeholder: string; + label?: string; + validators: FieldValidator[]; + defaultValue?: string; + ref: RefObject; + }[]; + onFormSubmit?: (form: FormRef) => Promise; + }; input: boolean; inputPlaceholder: string; defaultValue: string; diff --git a/apps/mobile/app/components/dialog/index.tsx b/apps/mobile/app/components/dialog/index.tsx index a9cff4864..9e8db457e 100644 --- a/apps/mobile/app/components/dialog/index.tsx +++ b/apps/mobile/app/components/dialog/index.tsx @@ -18,7 +18,13 @@ along with this program. If not, see . */ import { useThemeColors } from "@notesnook/theme"; -import React, { useCallback, useEffect, useRef, useState } from "react"; +import React, { + useCallback, + useEffect, + useRef, + useState, + RefObject +} from "react"; import { TextInput, View, ViewStyle } from "react-native"; import { DDS } from "../../services/device-detection"; import { @@ -34,6 +40,7 @@ import { sleep } from "../../utils/time"; import { Toast } from "../toast"; import { Button } from "../ui/button"; import Input from "../ui/input"; +import { FormInput, type FormRef } from "../ui/input/form-input"; import { Notice } from "../ui/notice"; import Seperator from "../ui/seperator"; import BaseDialog from "./base-dialog"; @@ -53,9 +60,34 @@ export const Dialog = ({ context = "global" }: { context?: string }) => { }); const inputRef = useRef(null); const [dialogInfo, setDialogInfo] = useState(); + const formRef = useRef(dialogInfo?.form?.formRef); + formRef.current = dialogInfo?.form?.formRef; const onPressPositive = async () => { - if (dialogInfo?.positivePress) { + // Handle form submission if form is available + if (dialogInfo?.form && formRef.current) { + inputRef.current?.blur(); + setLoading(true); + try { + const isValid = await formRef.current.validate(); + if (!isValid) { + setLoading(false); + return; + } + + if (dialogInfo.form.onFormSubmit) { + const result = await dialogInfo.form.onFormSubmit(formRef.current); + if (result === false) { + setLoading(false); + return; + } + } + } catch (e) { + /** Empty */ + } + setLoading(false); + } else if (dialogInfo?.positivePress) { + // Handle old input-based submission inputRef.current?.blur(); setLoading(true); let result = false; @@ -76,6 +108,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => { setChecked(false); values.current.inputValue = undefined; + formRef.current = undefined; setVisible(false); }; @@ -85,6 +118,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => { if (data.context !== context) return; setDialogInfo(data); setChecked(data.check?.defaultValue); + formRef.current = data?.form?.formRef; values.current.inputValue = data.defaultValue; setVisible(true); }, @@ -94,6 +128,7 @@ export const Dialog = ({ context = "global" }: { context?: string }) => { const hide = React.useCallback(() => { setChecked(false); values.current.inputValue = undefined; + formRef.current = undefined; setVisible(false); setDialogInfo(undefined); dialogInfo?.onClose?.(); @@ -134,19 +169,30 @@ export const Dialog = ({ context = "global" }: { context?: string }) => { ? false : dialogInfo.statusBarTranslucent } - bounce={!dialogInfo.input} + bounce={!dialogInfo.input && !dialogInfo.form} closeOnTouch={!dialogInfo.disableBackdropClosing} background={dialogInfo.background} transparent={ dialogInfo.transparent === undefined ? true : dialogInfo.transparent } onShow={async () => { - if (dialogInfo.input) { + if (dialogInfo.input && !dialogInfo.form) { inputRef.current?.setNativeProps({ text: dialogInfo.defaultValue }); await sleep(300); inputRef.current?.focus(); + } else if (dialogInfo.form) { + const items = dialogInfo.form?.items; + const firstItem = items[0]; + for (const item of items) { + if (item.defaultValue) { + item.ref.current?.setNativeProps({ + text: dialogInfo.defaultValue + }); + } + } + firstItem?.ref?.current?.focus(); } }} visible={true} @@ -170,7 +216,36 @@ export const Dialog = ({ context = "global" }: { context?: string }) => { /> - {dialogInfo.input ? ( + {dialogInfo.form ? ( + + {dialogInfo.form.items.map((item, index) => ( + } + validators={item.validators} + defaultValue={item.defaultValue} + secureTextEntry={dialogInfo.secureTextEntry} + onSubmitEditing={() => { + const nextItem = dialogInfo?.form?.items?.[index + 1]; + if (nextItem) { + nextItem?.ref.current?.focus(); + } else { + onPressPositive(); + } + }} + /> + ))} + + ) : dialogInfo.input ? ( { }} testID="input-value" secureTextEntry={dialogInfo.secureTextEntry} - //defaultValue={dialogInfo.defaultValue} + defaultValue={dialogInfo.defaultValue} onSubmit={() => { onPressPositive(); }} @@ -237,7 +312,10 @@ export const Dialog = ({ context = "global" }: { context?: string }) => { { + Walkthrough.present("prouser", false, true); + }, 500); } await PremiumService.setPremiumStatus(); useMessageStore.getState().setAnnouncement(); diff --git a/apps/mobile/app/screens/settings/settings-data.tsx b/apps/mobile/app/screens/settings/settings-data.tsx index c2369337c..3a8089e2b 100644 --- a/apps/mobile/app/screens/settings/settings-data.tsx +++ b/apps/mobile/app/screens/settings/settings-data.tsx @@ -31,9 +31,10 @@ import dayjs from "dayjs"; import React from "react"; import { Appearance, Linking, Platform } from "react-native"; import { getVersion } from "react-native-device-info"; +import { TextInput } from "react-native-gesture-handler"; import * as RNIap from "react-native-iap"; -import ScreenGuardModule from "react-native-screenguard"; import { DatabaseLogger, db } from "../../common/database"; +import { MMKV } from "../../common/database/mmkv"; import filesystem from "../../common/filesystem"; import { presentDialog } from "../../components/dialog/functions"; import { AppLockPassword } from "../../components/dialogs/applock-password"; @@ -42,43 +43,45 @@ import ExportNotesSheet from "../../components/sheets/export-notes"; import { Issue } from "../../components/sheets/github/issue"; import { Progress } from "../../components/sheets/progress"; import { Update } from "../../components/sheets/update"; +import { + createFormRef, + validators +} from "../../components/ui/input/form-input"; import { VaultStatusType, useVaultStatus } from "../../hooks/use-vault-status"; import { BackgroundSync } from "../../services/background-sync"; import BackupService from "../../services/backup"; import BiometricService from "../../services/biometrics"; import { ToastManager, + VaultRequestType, eSendEvent, eSubscribeEvent, openVault, - presentSheet, - VaultRequestType + presentSheet } from "../../services/event-manager"; import Navigation from "../../services/navigation"; import Notifications from "../../services/notifications"; import PremiumService from "../../services/premium"; import SettingsService from "../../services/settings"; import Sync from "../../services/sync"; +import { clearAllStores } from "../../stores"; +import { refreshAllStores } from "../../stores/create-db-collection-store"; import { useThemeStore } from "../../stores/use-theme-store"; import { useUserStore } from "../../stores/use-user-store"; +import { EDITOR_LINE_HEIGHT } from "../../utils/constants"; import { eAfterSync, eCloseSheet, eOpenRecoveryKeyDialog } from "../../utils/events"; -import { NotesnookModule } from "../../utils/notesnook-module"; import { sleep } from "../../utils/time"; +import { resetTabStore } from "../editor/tiptap/use-tab-store"; import { MFARecoveryCodes, MFASheet } from "./2fa"; import { useDragState } from "./editor/state"; import { verifyUser, verifyUserWithApplock } from "./functions"; import { logoutUser } from "./logout"; import { SettingSection } from "./types"; import { getTimeLeft } from "./user-section"; -import { EDITOR_LINE_HEIGHT } from "../../utils/constants"; -import { MMKV } from "../../common/database/mmkv"; -import { resetTabStore } from "../editor/tiptap/use-tab-store"; -import { clearAllStores } from "../../stores"; -import { refreshAllStores } from "../../stores/create-db-collection-store"; export const settingsGroups: SettingSection[] = [ { @@ -234,18 +237,29 @@ export const settingsGroups: SettingSection[] = [ presentDialog({ title: strings.redeemGiftCode(), paragraph: strings.redeemGiftCodeDesc(), - input: true, - inputPlaceholder: strings.code(), - positiveText: strings.redeem(), - positivePress: async (value) => { - db.subscriptions.redeemCode(value).catch((e) => { - ToastManager.show({ - heading: "Error redeeming code", - message: (e as Error).message, - type: "error" - }); - }); - } + form: { + formRef: createFormRef({ + code: "" + }), + items: [ + { + name: "code", + placeholder: strings.code(), + ref: React.createRef(), + validators: [validators.required(strings.giftCodeRequired())] + } + ], + onFormSubmit: async (form) => { + try { + await db.subscriptions.redeemCode(form.getValue("code")); + return true; + } catch (e) { + form.setError("code", (e as Error).message); + return false; + } + } + }, + positiveText: strings.redeem() }); } }, diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index a16f04684..311ee8c33 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -3101,6 +3101,10 @@ msgstr "Getting information" msgid "Getting recovery codes" msgstr "Getting recovery codes" +#: src/strings.ts:2663 +msgid "Gift code required" +msgstr "Gift code required" + #: src/strings.ts:2172 msgid "GNU GENERAL PUBLIC LICENSE Version 3" msgstr "GNU GENERAL PUBLIC LICENSE Version 3" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index 2d924d193..0f076d21c 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -3083,6 +3083,10 @@ msgstr "" msgid "Getting recovery codes" msgstr "" +#: src/strings.ts:2663 +msgid "Gift code required" +msgstr "" + #: src/strings.ts:2172 msgid "GNU GENERAL PUBLIC LICENSE Version 3" msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index eca1169f3..0b5621788 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2659,5 +2659,6 @@ Use this if changes from other devices are not appearing on this device. This wi t`Value must be between ${min} and ${max}`, passwordRequired: () => t`Password required`, confirmPasswordRequired: () => t`Confirm password required`, - enterAValidEmailAddress: () => t`Please enter a valid email address` + enterAValidEmailAddress: () => t`Please enter a valid email address`, + giftCodeRequired: () => t`Gift code required` };