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