diff --git a/apps/mobile/native/expo-module.config.json b/apps/mobile/native/expo-module.config.json
index 50199d33f..6976c4ef1 100644
--- a/apps/mobile/native/expo-module.config.json
+++ b/apps/mobile/native/expo-module.config.json
@@ -1,7 +1,7 @@
{
"platforms": ["apple", "android"],
"apple": {
- "modules": ["SharedWebViewModule", "HelperModule"]
+ "modules": ["SharedWebViewModule", "HelperModule", "ToasterModule"]
},
"android": {
"modules": []
diff --git a/apps/mobile/native/ios/FollowNative.podspec b/apps/mobile/native/ios/FollowNative.podspec
index 855b13a56..1e7701d9b 100644
--- a/apps/mobile/native/ios/FollowNative.podspec
+++ b/apps/mobile/native/ios/FollowNative.podspec
@@ -21,6 +21,7 @@ Pod::Spec.new do |s|
s.dependency 'ExpoModulesCore'
s.dependency 'SnapKit', '~> 5.7.0'
s.dependency 'SDWebImage', '~> 5.0'
+ s.dependency 'SPIndicator', '~> 1.0.0'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
diff --git a/apps/mobile/native/ios/Toaster/ToasterModule.swift b/apps/mobile/native/ios/Toaster/ToasterModule.swift
new file mode 100644
index 000000000..22a1e96d6
--- /dev/null
+++ b/apps/mobile/native/ios/Toaster/ToasterModule.swift
@@ -0,0 +1,63 @@
+//
+// ToasterModule.swift
+//
+// Created by Innei on 2025/2/21.
+//
+
+import ExpoModulesCore
+import SPIndicator
+
+enum ToastType: String, Enumerable {
+ case error
+ case info
+ case warn
+ case success
+}
+
+extension ToastType {
+ func type() -> SPIndicatorIconPreset {
+ switch self
+ {
+ case .error: .error
+ case .warn: .custom(UIImage(systemName: "exclamationmark.triangle")!.withTintColor(.orange))
+ case .info: .custom(UIImage(systemName: "info.circle")!.withTintColor(.blue))
+ case .success: .done
+ }
+ }
+
+ func haptic() -> SPIndicatorHaptic {
+ switch self {
+ case .error: .error
+ case .info: .success
+ case .warn: .warning
+ case .success: .error
+ }
+ }
+}
+struct ToastOptions: Record {
+ @Field
+ var message: String?
+ @Field
+ var type: ToastType = .info
+
+ @Field
+ var duration: Double = 1.5
+ @Field
+ var title: String
+
+}
+
+public class ToasterModule: Module {
+ public func definition() -> ModuleDefinition {
+ Name("Toaster")
+
+ Function("toast") { (value: ToastOptions) in
+
+ DispatchQueue.main.sync {
+ let indicatorView = SPIndicatorView(
+ title: value.title, message: value.message, preset: value.type.type())
+ indicatorView.present(duration: value.duration, haptic: value.type.haptic())
+ }
+ }
+ }
+}
diff --git a/apps/mobile/src/lib/toast.tsx b/apps/mobile/src/lib/toast.tsx
index 31c500b1d..38dca77b3 100644
--- a/apps/mobile/src/lib/toast.tsx
+++ b/apps/mobile/src/lib/toast.tsx
@@ -1,3 +1,6 @@
+import { requireNativeModule } from "expo"
+import { Platform } from "react-native"
+
import { ToastManager } from "../components/ui/toast/manager"
import type { ToastProps } from "../components/ui/toast/types"
@@ -11,11 +14,32 @@ type Toast = {
success: (message: string, options?: CommandToastOptions) => void
info: (message: string, options?: CommandToastOptions) => void
}
+
+interface NativeToasterOptions {
+ title: string
+ message: string
+ type: "error" | "success" | "info" | "warn"
+ /**
+ * seconds
+ */
+ duration?: number
+}
export const toast = {
show: toastInstance.show.bind(toastInstance),
} as Toast
;(["error", "success", "info"] as const).forEach((type) => {
toast[type] = (message: string, options: CommandToastOptions = {}) => {
+ if (Platform.OS === "ios") {
+ const NativeToaster = requireNativeModule("Toaster")
+ NativeToaster.toast({
+ title: "",
+ message,
+ type,
+ duration: options.duration ? options.duration / 1000 : 1.5,
+ } as NativeToasterOptions)
+ return
+ }
+
toastInstance.show({
type,
message,
diff --git a/apps/mobile/src/screens/(headless)/debug.tsx b/apps/mobile/src/screens/(headless)/debug.tsx
index f987a3740..439dfb1b8 100644
--- a/apps/mobile/src/screens/(headless)/debug.tsx
+++ b/apps/mobile/src/screens/(headless)/debug.tsx
@@ -101,11 +101,7 @@ export default function DebugPanel() {
{
title: "Toast",
onPress: () => {
- toast.show({
- message: "Hello, world!".repeat(10),
- type: "success",
- variant: "center-replace",
- })
+ toast.error("Hello, world!".repeat(10))
},
},
{
diff --git a/apps/mobile/src/screens/(headless)/login.tsx b/apps/mobile/src/screens/(headless)/login.tsx
index c80da5552..eb991d92e 100644
--- a/apps/mobile/src/screens/(headless)/login.tsx
+++ b/apps/mobile/src/screens/(headless)/login.tsx
@@ -6,7 +6,7 @@ import { useWhoami } from "@/src/store/user/hooks"
export default function LoginPage() {
const whoami = useWhoami()
- if (whoami?.id) {
+ if (whoami?.id && !__DEV__) {
return
}
diff --git a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx
index 28c760da6..aaa4328af 100644
--- a/apps/mobile/src/screens/(stack)/(tabs)/index.tsx
+++ b/apps/mobile/src/screens/(stack)/(tabs)/index.tsx
@@ -7,5 +7,6 @@ export default function Index() {
useEffect(() => {
prepareEntryRenderWebView()
}, [])
+
return
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 76f526cc0..82a0ab7f8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4004,8 +4004,8 @@ packages:
'@radix-ui/react-avatar@1.1.3':
resolution: {integrity: sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==}
peerDependencies:
- '@types/react': 18.3.12
- '@types/react-dom': 18.3.1
+ '@types/react': '*'
+ '@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@@ -4057,9 +4057,9 @@ packages:
'@radix-ui/react-context-menu@2.2.6':
resolution: {integrity: sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==}
peerDependencies:
- '@types/react': 18.3.12
- '@types/react-dom': 18.3.1
- react: 18.3.1
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@@ -4070,7 +4070,7 @@ packages:
'@radix-ui/react-context@1.1.1':
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
peerDependencies:
- '@types/react': npm:@types/react@^18.3.12
+ '@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@@ -4101,8 +4101,8 @@ packages:
'@radix-ui/react-dismissable-layer@1.1.5':
resolution: {integrity: sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==}
peerDependencies:
- '@types/react': 18.3.12
- '@types/react-dom': 18.3.1
+ '@types/react': '*'
+ '@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@@ -4127,7 +4127,7 @@ packages:
'@radix-ui/react-focus-guards@1.1.1':
resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==}
peerDependencies:
- '@types/react': npm:@types/react@^18.3.12
+ '@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@@ -4184,8 +4184,8 @@ packages:
'@radix-ui/react-menu@2.1.6':
resolution: {integrity: sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==}
peerDependencies:
- '@types/react': 18.3.12
- '@types/react-dom': 18.3.1
+ '@types/react': '*'
+ '@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@@ -4223,8 +4223,8 @@ packages:
'@radix-ui/react-popper@1.2.2':
resolution: {integrity: sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==}
peerDependencies:
- '@types/react': 18.3.12
- '@types/react-dom': 18.3.1
+ '@types/react': '*'
+ '@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@@ -4236,8 +4236,8 @@ packages:
'@radix-ui/react-portal@1.1.4':
resolution: {integrity: sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==}
peerDependencies:
- '@types/react': 18.3.12
- '@types/react-dom': 18.3.1
+ '@types/react': '*'
+ '@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@@ -4288,8 +4288,8 @@ packages:
'@radix-ui/react-roving-focus@1.1.2':
resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==}
peerDependencies:
- '@types/react': 18.3.12
- '@types/react-dom': 18.3.1
+ '@types/react': '*'
+ '@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@@ -4406,7 +4406,7 @@ packages:
'@radix-ui/react-use-callback-ref@1.1.0':
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
- '@types/react': npm:@types/react@^18.3.12
+ '@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@@ -4415,7 +4415,7 @@ packages:
'@radix-ui/react-use-controllable-state@1.1.0':
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
peerDependencies:
- '@types/react': npm:@types/react@^18.3.12
+ '@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
@@ -4469,8 +4469,8 @@ packages:
'@radix-ui/react-visually-hidden@1.1.2':
resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==}
peerDependencies:
- '@types/react': 18.3.12
- '@types/react-dom': 18.3.1
+ '@types/react': '*'
+ '@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
@@ -4552,7 +4552,7 @@ packages:
'@react-native-menu/menu@1.2.2':
resolution: {integrity: sha512-Uk65PAhwNkCVBAqJu5t2H9biV+m0JLwJc7m3v2X2A/W8SFJmUqYabBsLH4fOWKI3a7kkR9QDT6HruliIKSfM8w==}
peerDependencies:
- react: 18.3.1
+ react: '*'
react-native: '*'
'@react-native-picker/picker@2.11.0':
@@ -5263,7 +5263,7 @@ packages:
'@tanstack/react-query@5.66.7':
resolution: {integrity: sha512-qd3q/tUpF2K1xItfPZddk1k/8pSXnovg41XyCqJgPoyYEirMBtB0sVEVVQ/CsAOngzgWtBPXimVf4q4kM9uO6A==}
peerDependencies:
- react: 18.3.1
+ react: ^18 || ^19
'@tanstack/react-virtual@3.13.0':
resolution: {integrity: sha512-CchF0NlLIowiM2GxtsoKBkXA4uqSnY2KvnXo+kyUFD4a4ll6+J0qzoRsUPMwXV/H26lRsxgJIr/YmjYum2oEjg==}
@@ -8486,7 +8486,7 @@ packages:
resolution: {integrity: sha512-LUnfrddmee1xLOkyG2NN1l9xQbtvMX3fbM1brEGHg0SKSndvjod3FQdhTzZEYAariqW2RSxQR8v1IsheIoLQXg==}
peerDependencies:
expo: '*'
- react-native: '*'
+ react-native: npm:react-native@0.77.0
expo-module-scripts@4.0.4:
resolution: {integrity: sha512-ytFufVi7HTFLxxf8fbtz3DJuegq3489WPk9pz8Yqm3tYfZ+6/yufPYxLvk6N8b4yBW05oNr/Cqp7MWLpm0xPcA==}
@@ -12422,7 +12422,7 @@ packages:
resolution: {integrity: sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ==}
engines: {node: '>= 18.0.0'}
peerDependencies:
- react: 18.3.1
+ react: '*'
react-native: '*'
react-native-gesture-handler: '>=2.0.0'
react-native-reanimated: '>=3.0.0'
@@ -12536,7 +12536,7 @@ packages:
resolution: {integrity: sha512-9PPREdwH3tMR8otUK1voiFw9AFNtC37Del2fZwbCMBKjOyClC+f3Fwp7KqUouP0A5c3zjEc3T8TetBGQGPQE9A==}
peerDependencies:
react: '*'
- react-native: npm:react-native@0.77.0
+ react-native: '*'
react-native-gesture-handler: '*'
react-native-reanimated: '*'
@@ -12550,7 +12550,7 @@ packages:
resolution: {integrity: sha512-E5N/eK/+HtAVJUAzXpm1cWz8ROheV9jb0TI6h2bM+333U+DWibTTnT2T1122FkCoXLXIYavtm2FR2if+5jH8cA==}
peerDependencies:
react: '>=16.8.6'
- react-native: npm:react-native@0.77.0
+ react-native: '>=0.60.0-rc.2'
react-native-windows: '>=0.63.0'
shaka-player: ^4.7.9
peerDependenciesMeta:
@@ -12564,13 +12564,13 @@ packages:
peerDependencies:
nativewind: '>=4.1.0'
react: '>=18.0.0'
- react-native: '>=0.76.0'
+ react-native: npm:react-native@0.77.0
tailwindcss: '>=3.0.0'
react-native-volume-manager@2.0.8:
resolution: {integrity: sha512-aZM47/mYkdQ4CbXpKYO6Ajiczv7fxbQXZ9c0H8gRuQUaS3OCz/MZABer6o9aDWq0KMNsQ7q7GVFLRPnSSeeMmw==}
peerDependencies:
- react: 18.3.1
+ react: '*'
react-native: '*'
react-native-web@0.19.13: