diff --git a/apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorView.swift b/apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorView.swift index 24aa4fb9d..2457521d1 100644 --- a/apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorView.swift +++ b/apps/mobile/native/ios/Packages/SPIndicator/SPIndicatorView.swift @@ -148,7 +148,7 @@ open class SPIndicatorView: UIView { let label = UILabel() label.font = UIFont.preferredFont( forTextStyle: .footnote, weight: .semibold, addPoints: 0) - label.numberOfLines = 0 + label.numberOfLines = 3 let style = NSMutableParagraphStyle() style.lineBreakMode = .byTruncatingTail style.lineSpacing = 2 diff --git a/apps/mobile/src/icons/key_2_cute_re.tsx b/apps/mobile/src/icons/key_2_cute_re.tsx new file mode 100644 index 000000000..db30e919e --- /dev/null +++ b/apps/mobile/src/icons/key_2_cute_re.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import Svg, { Path } from "react-native-svg" + +interface Key2CuteReIconProps { + width?: number + height?: number + color?: string +} + +export const Key2CuteReIcon = ({ + width = 24, + height = 24, + color = "#10161F", +}: Key2CuteReIconProps) => { + return ( + + + + + ) +} diff --git a/apps/mobile/src/lib/dialog.tsx b/apps/mobile/src/lib/dialog.tsx index cb7268057..1ccaf3bd8 100644 --- a/apps/mobile/src/lib/dialog.tsx +++ b/apps/mobile/src/lib/dialog.tsx @@ -37,11 +37,22 @@ export interface DialogProps { id: string } -const entering = SlideInUp.springify().damping(15).stiffness(100) +interface ShowDialogOptions { + override?: { + onClose?: (ctx: Ctx & DialogContextType) => void + onConfirm?: (ctx: Ctx & DialogContextType) => void + cancelText?: string + confirmText?: string + } +} + +const entering = SlideInUp.springify().damping(16.5).stiffness(100) const exiting = SlideOutUp.duration(200) type DialogContextType = { dismiss: () => void + bizOnConfirm: (() => void) | null + bizOnCancel: (() => void) | null } const DialogDynamicButtonActionContext = createContext<{ @@ -61,8 +72,7 @@ const SetDialogDynamicButtonActionContext = createContext<{ }) const DialogContext = createContext(null) -export type DialogComponent = FC & - Omit, "content"> +export type DialogComponent = FC<{ ctx: Ctx }> & Omit, "content"> class DialogStatic { useDialogContext = () => { // eslint-disable-next-line react-hooks/rules-of-hooks @@ -110,8 +120,13 @@ class DialogStatic { return null } - show(propsOrComponent: DialogProps | DialogComponent) { + show( + propsOrComponent: DialogProps | DialogComponent, + options?: ShowDialogOptions, + ) { const isExist = this.currentStackedDialogs.has(propsOrComponent.id) + + const override = options?.override if (isExist) { return } @@ -122,7 +137,19 @@ class DialogStatic { const dismiss = () => this.destroy(props.id, siblings) - const reactCtx = { dismiss } + const reactCtx: DialogContextType = { + dismiss, + get bizOnConfirm() { + return () => { + handleConfirm() + } + }, + get bizOnCancel() { + return () => { + handleClose() + } + }, + } const mergeCtx = (ctx: Ctx) => ({ ...ctx, ...reactCtx }) @@ -131,20 +158,27 @@ class DialogStatic { "content" in propsOrComponent ? propsOrComponent.content : createElement(propsOrComponent, { - dismiss, ctx, }) const handleClose = () => { dismiss() setTimeout(() => { - props.onClose?.(mergeCtx(ctx)) + if (override?.onClose) { + override.onClose(mergeCtx(ctx)) + } else { + props.onClose?.(mergeCtx(ctx)) + } }, 16) } const handleConfirm = () => { - props.onConfirm?.(mergeCtx(ctx)) - handleClose() + if (override?.onConfirm) { + override.onConfirm(mergeCtx(ctx)) + } else { + props.onConfirm?.(mergeCtx(ctx)) + handleClose() + } } const Header = props.HeaderComponent ? ( @@ -152,9 +186,9 @@ class DialogStatic { title: props.title ?? "", onClose: handleClose, }) - ) : ( + ) : props.title ? ( - ) + ) : null const siblings = new RootSiblings( ( @@ -168,23 +202,24 @@ class DialogStatic { {Header} - + {children} diff --git a/apps/mobile/src/modules/dialogs/AddFeedDialog.tsx b/apps/mobile/src/modules/dialogs/AddFeedDialog.tsx index 1582a794f..8cec02fb9 100644 --- a/apps/mobile/src/modules/dialogs/AddFeedDialog.tsx +++ b/apps/mobile/src/modules/dialogs/AddFeedDialog.tsx @@ -4,13 +4,15 @@ import { useColor } from "react-native-uikit-colors" import { LinkCuteReIcon } from "@/src/icons/link_cute_re" import type { DialogComponent } from "@/src/lib/dialog" +import { Dialog } from "@/src/lib/dialog" import { accentColor } from "@/src/theme/colors" export const AddFeedDialog: DialogComponent<{ url: string -}> = ({ dismiss, ctx }) => { +}> = ({ ctx }) => { const label = useColor("label") + const { dismiss } = Dialog.useDialogContext()! const handleAdd = () => { dismiss() const value = ctx.url diff --git a/apps/mobile/src/modules/dialogs/ConfirmPasswordDialog.tsx b/apps/mobile/src/modules/dialogs/ConfirmPasswordDialog.tsx new file mode 100644 index 000000000..1fe7b6e63 --- /dev/null +++ b/apps/mobile/src/modules/dialogs/ConfirmPasswordDialog.tsx @@ -0,0 +1,41 @@ +import { Text, View } from "react-native" +import { useColor } from "react-native-uikit-colors" + +import { PlainTextField } from "@/src/components/ui/form/TextField" +import { Key2CuteReIcon } from "@/src/icons/key_2_cute_re" +import type { DialogComponent } from "@/src/lib/dialog" +import { Dialog } from "@/src/lib/dialog" + +export const ConfirmPasswordDialog: DialogComponent<{ + password: string +}> = ({ ctx }) => { + const label = useColor("label") + const { bizOnConfirm } = Dialog.useDialogContext()! + return ( + + + + Confirm your password to continue + + (ctx.password = text)} + returnKeyType="done" + onSubmitEditing={() => { + bizOnConfirm?.() + }} + /> + + ) +} + +ConfirmPasswordDialog.id = "confirm-password-dialog" +ConfirmPasswordDialog.confirmText = "Confirm" +ConfirmPasswordDialog.cancelText = "Cancel" +ConfirmPasswordDialog.onConfirm = (ctx) => { + ctx.dismiss() +} diff --git a/apps/mobile/src/modules/settings/routes/Account.tsx b/apps/mobile/src/modules/settings/routes/Account.tsx index b075b0081..7c5e2f82d 100644 --- a/apps/mobile/src/modules/settings/routes/Account.tsx +++ b/apps/mobile/src/modules/settings/routes/Account.tsx @@ -28,12 +28,15 @@ import { getProviders, linkSocial, signOut, + twoFactor, unlinkAccount, } from "@/src/lib/auth" +import { Dialog } from "@/src/lib/dialog" import { openLink } from "@/src/lib/native" import { toast } from "@/src/lib/toast" import { useWhoami } from "@/src/store/user/hooks" +import { ConfirmPasswordDialog } from "../../dialogs/ConfirmPasswordDialog" import { useSettingsNavigation } from "../hooks" type Account = { @@ -254,7 +257,26 @@ const SecuritySection = () => { textClassName="text-left" label="Setting 2FA" onPress={() => { - router.navigate("Setting2FA") + Dialog.show(ConfirmPasswordDialog, { + override: { + async onConfirm(ctx) { + const { password } = ctx + ctx.dismiss() + const res = await twoFactor.enable({ password }) + if (res.error?.message) { + toast.error("Invalid password or something went wrong") + return + } + if (res.data && "totpURI" in res.data) { + router.navigate("Setting2FA", { + totpURI: res.data.totpURI, + }) + } else { + toast.error("Failed to enable 2FA") + } + }, + }, + }) }} /> diff --git a/apps/mobile/src/modules/settings/routes/Setting2FAScreen.tsx b/apps/mobile/src/modules/settings/routes/Setting2FAScreen.tsx index bd3ea6070..b0f885760 100644 --- a/apps/mobile/src/modules/settings/routes/Setting2FAScreen.tsx +++ b/apps/mobile/src/modules/settings/routes/Setting2FAScreen.tsx @@ -1,3 +1,23 @@ -export const Setting2FAScreen = () => { - return null +import type { RouteProp } from "@react-navigation/native" +import { View } from "react-native" + +import { + NavigationBlurEffectHeader, + SafeNavigationScrollView, +} from "@/src/components/layouts/views/SafeNavigationScrollView" + +import type { SettingsStackParamList } from "../types" + +export const Setting2FAScreen = ({ + route: _route, +}: { + route: RouteProp +}) => { + return ( + + + + + + ) } diff --git a/apps/mobile/src/modules/settings/routes/index.tsx b/apps/mobile/src/modules/settings/routes/index.tsx index 1f81b25d9..44ecfa44c 100644 --- a/apps/mobile/src/modules/settings/routes/index.tsx +++ b/apps/mobile/src/modules/settings/routes/index.tsx @@ -35,6 +35,8 @@ const SettingFlatRoutes = (Stack: TypedNavigator) => { + + {/* @ts-expect-error */} ) diff --git a/apps/mobile/src/modules/settings/types.ts b/apps/mobile/src/modules/settings/types.ts index 7ae1ae69b..ef8cd6ae5 100644 --- a/apps/mobile/src/modules/settings/types.ts +++ b/apps/mobile/src/modules/settings/types.ts @@ -14,5 +14,5 @@ export type SettingsStackParamList = { ManageList: { id: string } EditProfile: undefined ResetPassword: undefined - Setting2FA: undefined + Setting2FA: { totpURI: string } } diff --git a/icons/mgc/key_2_cute_re.svg b/icons/mgc/key_2_cute_re.svg new file mode 100644 index 000000000..f6184e64a --- /dev/null +++ b/icons/mgc/key_2_cute_re.svg @@ -0,0 +1 @@ + \ No newline at end of file