From 8dbb6343fb299a7b1e15288405d45bfb72dec09b Mon Sep 17 00:00:00 2001 From: Innei Date: Fri, 21 Feb 2025 16:52:09 +0800 Subject: [PATCH] feat(mobile): add native iOS toast module with SPIndicator Signed-off-by: Innei --- apps/mobile/native/expo-module.config.json | 2 +- apps/mobile/native/ios/FollowNative.podspec | 1 + .../native/ios/Toaster/ToasterModule.swift | 63 +++++++++++++++++++ apps/mobile/src/lib/toast.tsx | 24 +++++++ apps/mobile/src/screens/(headless)/debug.tsx | 6 +- apps/mobile/src/screens/(headless)/login.tsx | 2 +- .../src/screens/(stack)/(tabs)/index.tsx | 1 + pnpm-lock.yaml | 58 ++++++++--------- 8 files changed, 121 insertions(+), 36 deletions(-) create mode 100644 apps/mobile/native/ios/Toaster/ToasterModule.swift diff --git a/apps/mobile/native/expo-module.config.json b/apps/mobile/native/expo-module.config.json index 50199d33fc..6976c4ef1e 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 855b13a564..1e7701d9b6 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 0000000000..22a1e96d64 --- /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 31c500b1de..38dca77b32 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 f987a3740f..439dfb1b8f 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 c80da55523..eb991d92ea 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 28c760da64..aaa4328afe 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 76f526cc0a..82a0ab7f85 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: