feat(mobile): enhance dialog functionality and add ConfirmPasswordDialog

- Introduce ShowDialogOptions interface for customizable dialog actions
- Update Dialog component to support onClose and onConfirm overrides
- Add ConfirmPasswordDialog for password confirmation with custom actions
- Integrate ConfirmPasswordDialog into Account settings for 2FA setup
- Add new Key2CuteReIcon component for password confirmation UI
- Update package.json and other files for new icon and dialog features

Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
Innei 2025-03-13 00:07:45 +08:00
parent 6438667f25
commit d9ea5754cb
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
10 changed files with 168 additions and 21 deletions

View File

@ -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

View File

@ -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 (
<Svg width={width} height={height} fill="none" viewBox="0 0 24 24">
<Path fill={color} d="M16.24 8.26a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0" />
<Path
fill={color}
d="m11.145 5.074-.707-.707zM4.306 16.155l-.707-.707zm8.641-2.102.379-.925zm5.269-1.908a4.501 4.501 0 0 1-4.89.983l-.758 1.85a6.498 6.498 0 0 0 7.062-1.419zm0-6.364a4.5 4.5 0 0 1 0 6.364l1.414 1.414a6.5 6.5 0 0 0 0-9.192zm-6.364 0a4.5 4.5 0 0 1 6.364 0l1.414-1.414a6.5 6.5 0 0 0-9.192 0zm-1.3 3.592a4.489 4.489 0 0 1 1.3-3.592l-1.414-1.414A6.489 6.489 0 0 0 8.56 9.554zm-5.539 7.49 4.63-4.63-1.414-1.415-4.63 4.63zM6.55 18.57h-.83v2h.829zm5.74-5.657h-.083v2h.083zm2.95-4.653a.5.5 0 0 1 .5-.5v2a1.5 1.5 0 0 0 1.5-1.5zm.5-.5a.5.5 0 0 1 .5.5h-2a1.5 1.5 0 0 0 1.5 1.5zm.5.5a.5.5 0 0 1-.5.5v-2a1.5 1.5 0 0 0-1.5 1.5zm-.5.5a.5.5 0 0 1-.5-.5h2a1.5 1.5 0 0 0-1.5-1.5zm-3.949 6.567c0-.229.186-.414.415-.414v-2a2.414 2.414 0 0 0-2.415 2.414zm-2.414 2.414a2.414 2.414 0 0 0 2.414-2.414h-2a.414.414 0 0 1-.414.414zm-.414.414c0-.228.185-.414.414-.414v-2a2.414 2.414 0 0 0-2.414 2.414zM6.549 20.57a2.414 2.414 0 0 0 2.414-2.415h-2a.414.414 0 0 1-.414.415zm-2.95-5.122a3 3 0 0 0-.879 2.122h2a1 1 0 0 1 .293-.707zM8.56 9.554c.052.58-.087 1.02-.332 1.265l1.414 1.414c.817-.818.993-1.944.91-2.86zM2.72 17.57a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1zm10.605-4.442a2.74 2.74 0 0 0-1.037-.215v2a.75.75 0 0 1 .28.066z"
/>
</Svg>
)
}

View File

@ -37,11 +37,22 @@ export interface DialogProps<Ctx> {
id: string
}
const entering = SlideInUp.springify().damping(15).stiffness(100)
interface ShowDialogOptions<Ctx> {
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<DialogContextType | null>(null)
export type DialogComponent<Ctx = unknown> = FC<DialogContextType & { ctx: Ctx }> &
Omit<DialogProps<Ctx>, "content">
export type DialogComponent<Ctx = unknown> = FC<{ ctx: Ctx }> & Omit<DialogProps<Ctx>, "content">
class DialogStatic {
useDialogContext = () => {
// eslint-disable-next-line react-hooks/rules-of-hooks
@ -110,8 +120,13 @@ class DialogStatic {
return null
}
show<Ctx>(propsOrComponent: DialogProps<Ctx> | DialogComponent<Ctx>) {
show<Ctx>(
propsOrComponent: DialogProps<Ctx> | DialogComponent<Ctx>,
options?: ShowDialogOptions<Ctx>,
) {
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 ? (
<DefaultHeader title={props.title} headerIcon={props.headerIcon} />
)
) : null
const siblings = new RootSiblings(
(
@ -168,23 +202,24 @@ class DialogStatic {
<SafeInsetTop />
<DialogDynamicButtonActionProvider>
{Header}
<View className="px-6 py-4">
<View className={cn("px-6 pb-4", Header ? "pt-4" : "pt-0")}>
<DialogContext.Provider value={reactCtx}>{children}</DialogContext.Provider>
</View>
<View className="flex-row gap-4 px-6 pb-4">
<DialogDynamicButtonAction
fallbackCaller={handleClose}
text={props.cancelText ?? "Cancel"}
text={override?.cancelText ?? props.cancelText ?? "Cancel"}
type="cancel"
textClassName={cn(props.variant === "destructive" && "font-bold")}
/>
<DialogDynamicButtonAction
fallbackCaller={handleConfirm}
text={props.confirmText ?? "Confirm"}
text={override?.confirmText ?? props.confirmText ?? "Confirm"}
type="confirm"
className={props.variant === "destructive" ? "bg-red" : "bg-accent"}
textClassName="text-white"
textClassName={cn("text-white", props.variant !== "destructive" && "font-bold")}
/>
</View>
</DialogDynamicButtonActionProvider>

View File

@ -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

View File

@ -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 (
<View>
<View className="flex-row items-center gap-2">
<Key2CuteReIcon color={label} height={20} width={20} />
<Text className="text-label text-base font-medium">Confirm your password to continue</Text>
</View>
<PlainTextField
autoFocus
autoCapitalize="none"
secureTextEntry
className="bg-system-background text-text my-3 rounded-xl p-2 px-4"
placeholder="Password"
onChangeText={(text) => (ctx.password = text)}
returnKeyType="done"
onSubmitEditing={() => {
bizOnConfirm?.()
}}
/>
</View>
)
}
ConfirmPasswordDialog.id = "confirm-password-dialog"
ConfirmPasswordDialog.confirmText = "Confirm"
ConfirmPasswordDialog.cancelText = "Cancel"
ConfirmPasswordDialog.onConfirm = (ctx) => {
ctx.dismiss()
}

View File

@ -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")
}
},
},
})
}}
/>
</GroupedInsetListCard>

View File

@ -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<SettingsStackParamList, "Setting2FA">
}) => {
return (
<SafeNavigationScrollView>
<NavigationBlurEffectHeader title="Setting 2FA" />
<View className="my-12 items-center justify-center" />
</SafeNavigationScrollView>
)
}

View File

@ -35,6 +35,8 @@ const SettingFlatRoutes = (Stack: TypedNavigator<any, any>) => {
<Stack.Screen key="ManageList" name="ManageList" component={ManageListScreen} />
<Stack.Screen key="EditProfile" name="EditProfile" component={EditProfileScreen} />
<Stack.Screen key="ResetPassword" name="ResetPassword" component={ResetPassword} />
{/* @ts-expect-error */}
<Stack.Screen key="Setting2FA" name="Setting2FA" component={Setting2FAScreen} />
</Stack.Group>
)

View File

@ -14,5 +14,5 @@ export type SettingsStackParamList = {
ManageList: { id: string }
EditProfile: undefined
ResetPassword: undefined
Setting2FA: undefined
Setting2FA: { totpURI: string }
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none"><path fill="#fff" fill-opacity=".01" d="M24 0v24H0V0z"/><path fill="#10161F" d="M16.24 8.26a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0"/><path fill="#10161F" d="m11.145 5.074-.707-.707zM4.306 16.155l-.707-.707zm8.641-2.102.379-.925zm5.269-1.908a4.501 4.501 0 0 1-4.89.983l-.758 1.85a6.498 6.498 0 0 0 7.062-1.419zm0-6.364a4.5 4.5 0 0 1 0 6.364l1.414 1.414a6.5 6.5 0 0 0 0-9.192zm-6.364 0a4.5 4.5 0 0 1 6.364 0l1.414-1.414a6.5 6.5 0 0 0-9.192 0zm-1.3 3.592a4.489 4.489 0 0 1 1.3-3.592l-1.414-1.414A6.489 6.489 0 0 0 8.56 9.554zm-5.539 7.49 4.63-4.63-1.414-1.415-4.63 4.63zM6.55 18.57h-.83v2h.829zm5.74-5.657h-.083v2h.083zm2.95-4.653a.5.5 0 0 1 .5-.5v2a1.5 1.5 0 0 0 1.5-1.5zm.5-.5a.5.5 0 0 1 .5.5h-2a1.5 1.5 0 0 0 1.5 1.5zm.5.5a.5.5 0 0 1-.5.5v-2a1.5 1.5 0 0 0-1.5 1.5zm-.5.5a.5.5 0 0 1-.5-.5h2a1.5 1.5 0 0 0-1.5-1.5zm-3.949 6.567c0-.229.186-.414.415-.414v-2a2.414 2.414 0 0 0-2.415 2.414zm-2.414 2.414a2.414 2.414 0 0 0 2.414-2.414h-2a.414.414 0 0 1-.414.414zm-.414.414c0-.228.185-.414.414-.414v-2a2.414 2.414 0 0 0-2.414 2.414zM6.549 20.57a2.414 2.414 0 0 0 2.414-2.415h-2a.414.414 0 0 1-.414.415zm-2.95-5.122a3 3 0 0 0-.879 2.122h2a1 1 0 0 1 .293-.707zM8.56 9.554c.052.58-.087 1.02-.332 1.265l1.414 1.414c.817-.818.993-1.944.91-2.86zM2.72 17.57a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1zm10.605-4.442a2.74 2.74 0 0 0-1.037-.215v2a.75.75 0 0 1 .28.066z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB