From 5ef3bdcad45bbb67c480bf53f3395dfbc5a38d20 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Wed, 6 May 2026 12:03:20 +0500 Subject: [PATCH 1/3] mobile: fix handling invalid gift code --- apps/mobile/app/screens/settings/settings-data.tsx | 12 ++++++++++-- packages/intl/locale/en.po | 4 ++++ packages/intl/locale/pseudo-LOCALE.po | 4 ++++ packages/intl/src/strings.ts | 3 ++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/mobile/app/screens/settings/settings-data.tsx b/apps/mobile/app/screens/settings/settings-data.tsx index c2369337c..0328b1240 100644 --- a/apps/mobile/app/screens/settings/settings-data.tsx +++ b/apps/mobile/app/screens/settings/settings-data.tsx @@ -238,13 +238,21 @@ export const settingsGroups: SettingSection[] = [ inputPlaceholder: strings.code(), positiveText: strings.redeem(), positivePress: async (value) => { - db.subscriptions.redeemCode(value).catch((e) => { + try { + if (!value || !value.trim()) { + throw new Error(strings.giftCodeRequired()); + } + + await db.subscriptions.redeemCode(value); + return true; + } catch (e) { ToastManager.show({ heading: "Error redeeming code", message: (e as Error).message, type: "error" }); - }); + return false; + } } }); } 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` }; From 276239dd2b015dbe59d851fe59f7308923a68e37 Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 7 May 2026 08:49:21 +0500 Subject: [PATCH 2/3] mobile: add form validation support in dialog inputs --- .../mobile/app/components/dialog/functions.ts | 16 +++- apps/mobile/app/components/dialog/index.tsx | 92 +++++++++++++++++-- .../app/screens/settings/settings-data.tsx | 60 ++++++------ 3 files changed, 133 insertions(+), 35 deletions(-) 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 }) => { { - try { - if (!value || !value.trim()) { - throw new Error(strings.giftCodeRequired()); + 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; } - - await db.subscriptions.redeemCode(value); - return true; - } catch (e) { - ToastManager.show({ - heading: "Error redeeming code", - message: (e as Error).message, - type: "error" - }); - return false; } - } + }, + positiveText: strings.redeem() }); } }, From 780276b30e83cf1d580e842be26ba967139d4fdc Mon Sep 17 00:00:00 2001 From: Ammar Ahmed Date: Thu, 7 May 2026 11:12:49 +0500 Subject: [PATCH 3/3] mobile: fix pro user walk through makes app get stuck --- apps/mobile/app/hooks/use-app-events.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/mobile/app/hooks/use-app-events.tsx b/apps/mobile/app/hooks/use-app-events.tsx index 53f1bff0f..16824bd5e 100644 --- a/apps/mobile/app/hooks/use-app-events.tsx +++ b/apps/mobile/app/hooks/use-app-events.tsx @@ -95,6 +95,7 @@ import { BETA } from "../utils/constants"; import { eAfterSync, eCloseSheet, + eCloseSimpleDialog, eEditorReset, eLoginSessionExpired, eOnLoadNote, @@ -286,7 +287,10 @@ const onUserSubscriptionStatusChanged = async ( subscription: subscription } }); - Walkthrough.present("prouser", false, true); + eSendEvent(eCloseSimpleDialog); + setTimeout(() => { + Walkthrough.present("prouser", false, true); + }, 500); } await PremiumService.setPremiumStatus(); useMessageStore.getState().setAnnouncement();