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:
parent
6438667f25
commit
d9ea5754cb
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,5 +14,5 @@ export type SettingsStackParamList = {
|
|||
ManageList: { id: string }
|
||||
EditProfile: undefined
|
||||
ResetPassword: undefined
|
||||
Setting2FA: undefined
|
||||
Setting2FA: { totpURI: string }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
Loading…
Reference in New Issue