From eff412604341b557e5fe0900801cf29be9579a63 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Tue, 25 Nov 2025 18:22:43 +0800 Subject: [PATCH] feat(ios): improve share extension UX and fix image reload issues Improve iOS share extension implementation to skip interstitial UI and fix issues with subsequent image shares not updating the view. iOS Share Extension Improvements: - Replace SLComposeServiceViewController with custom UIViewController to eliminate interstitial "Post" button UI - Use minimal URL (timesafari://) instead of deep link for app launch - Implement app lifecycle detection via Capacitor appStateChange listener instead of relying solely on deep links Deep Link and Navigation Fixes: - Remove "shared-photo" from deep link schemas (no longer needed) - Add empty path URL handling for share extension launches - Implement processing lock to prevent duplicate image processing - Add retry mechanism (300ms delay) to handle race conditions with AppDelegate writing temp files - Use router.replace() when already on /shared-photo route to force refresh - Clear old images from temp DB before storing new ones - Delete temp file immediately after reading to prevent stale data SharedPhotoView Component: - Add route watcher (@Watch) to reload image when fileName query parameter changes - Extract image loading logic into reusable loadSharedImage() method - Improve error handling to clear image state on failures This fixes the issue where sharing a second image while already on SharedPhotoView would display the previous image instead of the new one. --- doc/ios-share-extension-improvements.md | 283 ++++++++++++++++++ ios/App/App/AppDelegate.swift | 50 ++++ .../ShareViewController.swift | 76 ++--- src/interfaces/deepLinks.ts | 1 - src/main.capacitor.ts | 200 +++++++++++-- src/views/SharedPhotoView.vue | 30 +- 6 files changed, 565 insertions(+), 75 deletions(-) create mode 100644 doc/ios-share-extension-improvements.md diff --git a/doc/ios-share-extension-improvements.md b/doc/ios-share-extension-improvements.md new file mode 100644 index 00000000..6ef6e3cb --- /dev/null +++ b/doc/ios-share-extension-improvements.md @@ -0,0 +1,283 @@ +# iOS Share Extension Improvements + +**Date:** 2025-11-24 +**Purpose:** Explore alternatives to improve user experience by eliminating interstitial UI and simplifying app launch mechanism + +## Current Implementation Issues + +1. **Interstitial UI**: Users see `SLComposeServiceViewController` with a "Post" button before the app opens +2. **Deep Link Dependency**: App relies on deep link (`timesafari://shared-photo`) to detect shared images, even though data is already in App Group + +## Improvement 1: Skip Interstitial UI + +### Current Approach +- Uses `SLComposeServiceViewController` which shows a UI with "Post" button +- User must tap "Post" to proceed + +### Alternative: Custom UIViewController (Headless Processing) + +Replace `SLComposeServiceViewController` with a custom `UIViewController` that: +- Processes the image immediately in `viewDidLoad` +- Shows no UI (or minimal loading indicator) +- Opens the app automatically + +**Implementation:** + +```swift +import UIKit +import UniformTypeIdentifiers + +class ShareViewController: UIViewController { + + private let appGroupIdentifier = "group.app.timesafari" + private let sharedPhotoBase64Key = "sharedPhotoBase64" + private let sharedPhotoFileNameKey = "sharedPhotoFileName" + + override func viewDidLoad() { + super.viewDidLoad() + + // Process image immediately without showing UI + processAndOpenApp() + } + + private func processAndOpenApp() { + guard let extensionContext = extensionContext, + let inputItems = extensionContext.inputItems as? [NSExtensionItem] else { + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + return + } + + processSharedImage(from: inputItems) { [weak self] success in + guard let self = self else { + self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + return + } + + if success { + self.openMainApp() + } + + // Complete immediately - no UI shown + self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } + } + + private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) { + // ... (same implementation as current) + } + + private func openMainApp() { + guard let url = URL(string: "timesafari://shared-photo") else { + return + } + + var responder: UIResponder? = self + while responder != nil { + if let application = responder as? UIApplication { + application.open(url, options: [:], completionHandler: nil) + return + } + responder = responder?.next + } + + extensionContext?.open(url, completionHandler: nil) + } +} +``` + +**Info.plist Changes:** +- Already configured correctly with `NSExtensionPrincipalClass` +- No storyboard needed (already removed) + +**Benefits:** +- ✅ No interstitial UI - app opens immediately +- ✅ Faster user experience +- ✅ More seamless integration + +**Considerations:** +- ⚠️ User has less control (can't cancel easily) +- ⚠️ No visual feedback during processing (could add minimal loading indicator) +- ⚠️ Apple guidelines: Extensions should provide value even if they don't open the app + +## Improvement 2: Direct App Launch Without Deep Link + +### Current Approach +- Share Extension stores data in App Group UserDefaults +- Share Extension opens app via deep link (`timesafari://shared-photo`) +- App receives deep link → checks App Group → processes image + +### Alternative: App Lifecycle Detection + +Instead of using deep links, the app can check for shared data when it becomes active: + +**Option A: Check on App Activation** + +```swift +// In AppDelegate.swift +func applicationDidBecomeActive(_ application: UIApplication) { + // Check for shared image from Share Extension + if let sharedData = getSharedImageData() { + // Store in temp file for JS to read + writeSharedImageToTempFile(sharedData) + + // Navigate to shared-photo route directly + // This would need to be handled in JS layer + } +} +``` + +**Option B: Use Notification (More Reliable)** + +```swift +// In ShareViewController.swift (after storing data) +private func openMainApp() { + // Store a flag that image is ready + guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { + return + } + userDefaults.set(true, forKey: "sharedPhotoReady") + userDefaults.synchronize() + + // Open app (can use any URL scheme or even just launch the app) + guard let url = URL(string: "timesafari://") else { + return + } + + var responder: UIResponder? = self + while responder != nil { + if let application = responder as? UIApplication { + application.open(url, options: [:], completionHandler: nil) + return + } + responder = responder?.next + } +} + +// In AppDelegate.swift +func applicationDidBecomeActive(_ application: UIApplication) { + let appGroupIdentifier = "group.app.timesafari" + guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { + return + } + + // Check if shared photo is ready + if userDefaults.bool(forKey: "sharedPhotoReady") { + userDefaults.removeObject(forKey: "sharedPhotoReady") + userDefaults.synchronize() + + // Process shared image + if let sharedData = getSharedImageData() { + writeSharedImageToTempFile(sharedData) + + // Trigger JS to check for shared image + // This could be done via Capacitor App plugin or custom event + } + } +} +``` + +**Option C: Check on App Launch (Most Direct)** + +```swift +// In AppDelegate.swift +func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Check for shared image immediately on launch + checkForSharedImageOnLaunch() + + return true +} + +func applicationDidBecomeActive(_ application: UIApplication) { + // Also check when app becomes active (in case it was already running) + checkForSharedImageOnLaunch() +} + +private func checkForSharedImageOnLaunch() { + if let sharedData = getSharedImageData() { + writeSharedImageToTempFile(sharedData) + + // Post a notification or use Capacitor to notify JS + NotificationCenter.default.post(name: NSNotification.Name("SharedPhotoReady"), object: nil) + } +} +``` + +**JavaScript Integration:** + +```typescript +// In main.capacitor.ts +import { App } from '@capacitor/app'; + +// Listen for app becoming active +App.addListener('appStateChange', async ({ isActive }) => { + if (isActive) { + // Check for shared image when app becomes active + await checkAndStoreNativeSharedImage(); + } +}); + +// Also check on initial load +if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === 'ios') { + checkAndStoreNativeSharedImage().then(result => { + if (result.success) { + // Navigate to shared-photo route + router.push('/shared-photo' + (result.fileName ? `?fileName=${result.fileName}` : '')); + } + }); +} +``` + +**Benefits:** +- ✅ No deep link routing needed +- ✅ More direct data flow +- ✅ App can detect shared content even if it was already running +- ✅ Simpler URL scheme handling + +**Considerations:** +- ⚠️ Need to ensure app checks on both launch and activation +- ⚠️ May need to handle race conditions (app launching vs. share extension writing) +- ⚠️ Still need some way to open the app (minimal URL scheme still required) + +## Recommended Approach + +**Best of Both Worlds:** + +1. **Use Custom UIViewController** (Improvement 1) - Eliminates interstitial UI +2. **Use App Lifecycle Detection** (Improvement 2, Option C) - Direct data flow + +**Combined Implementation:** + +```swift +// ShareViewController.swift - Custom UIViewController +class ShareViewController: UIViewController { + // Process immediately in viewDidLoad + // Store data in App Group + // Open app with minimal URL (just "timesafari://") +} + +// AppDelegate.swift +func applicationDidBecomeActive(_ application: UIApplication) { + // Check for shared image + // If found, write to temp file and let JS handle navigation +} +``` + +**JavaScript:** +```typescript +// Check on app activation +App.addListener('appStateChange', async ({ isActive }) => { + if (isActive) { + const result = await checkAndStoreNativeSharedImage(); + if (result.success) { + router.push('/shared-photo' + (result.fileName ? `?fileName=${result.fileName}` : '')); + } + } +}); +``` + +This approach: +- ✅ No interstitial UI +- ✅ No deep link routing complexity +- ✅ Direct data flow via App Group +- ✅ Works whether app is running or launching fresh + diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index 33937d3f..a3b0d767 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -32,6 +32,56 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationDidBecomeActive(_ application: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + + // Check for shared image from Share Extension when app becomes active + checkForSharedImageOnActivation() + } + + /** + * Check for shared image when app launches or becomes active + * This allows the app to detect shared images without requiring a deep link + */ + private func checkForSharedImageOnActivation() { + let appGroupIdentifier = "group.app.timesafari" + guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { + return + } + + // Check if shared photo is ready + if userDefaults.bool(forKey: "sharedPhotoReady") { + // Clear the flag + userDefaults.removeObject(forKey: "sharedPhotoReady") + userDefaults.synchronize() + + // Get and process shared image data + if let sharedData = getSharedImageData() { + writeSharedImageToTempFile(sharedData) + + // Post notification for JavaScript to handle navigation + NotificationCenter.default.post(name: NSNotification.Name("SharedPhotoReady"), object: nil) + } + } + } + + /** + * Write shared image data to temp file for JavaScript to read + */ + private func writeSharedImageToTempFile(_ sharedData: [String: String]) { + let fileManager = FileManager.default + guard let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { + return + } + + let tempFileURL = documentsDir.appendingPathComponent("timesafari_shared_photo.json") + + let jsonData: [String: String] = [ + "base64": sharedData["base64"] ?? "", + "fileName": sharedData["fileName"] ?? "" + ] + + if let json = try? JSONSerialization.data(withJSONObject: jsonData, options: []) { + try? json.write(to: tempFileURL) + } } func applicationWillTerminate(_ application: UIApplication) { diff --git a/ios/App/TimeSafariShareExtension/ShareViewController.swift b/ios/App/TimeSafariShareExtension/ShareViewController.swift index 9090d128..316ccc99 100644 --- a/ios/App/TimeSafariShareExtension/ShareViewController.swift +++ b/ios/App/TimeSafariShareExtension/ShareViewController.swift @@ -6,10 +6,9 @@ // import UIKit -import Social import UniformTypeIdentifiers -class ShareViewController: SLComposeServiceViewController { +class ShareViewController: UIViewController { private let appGroupIdentifier = "group.app.timesafari" private let sharedPhotoBase64Key = "sharedPhotoBase64" @@ -18,64 +17,46 @@ class ShareViewController: SLComposeServiceViewController { override func viewDidLoad() { super.viewDidLoad() - // Set placeholder text (required for SLComposeServiceViewController) - self.placeholder = "Share image to TimeSafari" + // Set a minimal background (transparent or loading indicator) + view.backgroundColor = .systemBackground - // Validate content on load - self.validateContent() + // Process image immediately without showing UI + processAndOpenApp() } - override func isContentValid() -> Bool { - // Validate that we have image attachments - guard let extensionContext = extensionContext else { - return false - } - - guard let inputItems = extensionContext.inputItems as? [NSExtensionItem] else { - return false - } - - for item in inputItems { - if let attachments = item.attachments { - for attachment in attachments { - if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) { - return true - } - } - } - } - - return false - } - - override func didSelectPost() { - // Extract and process the shared image - guard let extensionContext = extensionContext else { + private func processAndOpenApp() { + // extensionContext is automatically available on UIViewController when used as extension principal class + guard let context = extensionContext, + let inputItems = context.inputItems as? [NSExtensionItem] else { + extensionContext?.completeRequest(returningItems: [], completionHandler: nil) return } - guard let inputItems = extensionContext.inputItems as? [NSExtensionItem] else { - extensionContext.completeRequest(returningItems: [], completionHandler: nil) - return - } - - // Process the first image found processSharedImage(from: inputItems) { [weak self] success in - guard let self = self else { - extensionContext.completeRequest(returningItems: [], completionHandler: nil) + guard let self = self, let context = self.extensionContext else { return } if success { - // Open the main app via deep link + // Set flag that shared photo is ready + self.setSharedPhotoReadyFlag() + // Open the main app (using minimal URL - app will detect shared data on activation) self.openMainApp() } - // Complete the extension context - extensionContext.completeRequest(returningItems: [], completionHandler: nil) + // Complete immediately - no UI shown + context.completeRequest(returningItems: [], completionHandler: nil) } } + private func setSharedPhotoReadyFlag() { + guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else { + return + } + userDefaults.set(true, forKey: "sharedPhotoReady") + userDefaults.synchronize() + } + private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) { // Find the first image attachment for item in items { @@ -144,8 +125,8 @@ class ShareViewController: SLComposeServiceViewController { } private func openMainApp() { - // Open the main app via deep link - guard let url = URL(string: "timesafari://shared-photo") else { + // Open the main app with minimal URL - app will detect shared data on activation + guard let url = URL(string: "timesafari://") else { return } @@ -162,9 +143,4 @@ class ShareViewController: SLComposeServiceViewController { extensionContext?.open(url, completionHandler: nil) } - override func configurationItems() -> [Any]! { - // No additional configuration options needed - return [] - } - } diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts index 60c0826c..0fe5c68d 100644 --- a/src/interfaces/deepLinks.ts +++ b/src/interfaces/deepLinks.ts @@ -68,7 +68,6 @@ export const deepLinkPathSchemas = { "user-profile": z.object({ id: z.string(), }), - "shared-photo": z.object({}), }; export const deepLinkQuerySchemas = { diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts index 3ef253a5..8045b048 100644 --- a/src/main.capacitor.ts +++ b/src/main.capacitor.ts @@ -55,6 +55,9 @@ window.addEventListener("unhandledrejection", (event) => { const deepLinkHandler = new DeepLinkHandler(router); +// Lock to prevent duplicate processing of shared images +let isProcessingSharedImage = false; + /** * Handles deep link routing for the application * Processes URLs in the format timesafari:/// @@ -79,11 +82,22 @@ async function checkAndStoreNativeSharedImage(): Promise<{ success: boolean; fileName?: string; }> { - if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== "ios") { + // Prevent duplicate processing + if (isProcessingSharedImage) { + logger.debug( + "[Main] ⏸️ Shared image processing already in progress, skipping", + ); return { success: false }; } + isProcessingSharedImage = true; + try { + if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== "ios") { + isProcessingSharedImage = false; + return { success: false }; + } + logger.debug("[Main] Checking for iOS shared image from App Group"); // Use Capacitor's native bridge to call the ShareImagePlugin @@ -92,6 +106,7 @@ async function checkAndStoreNativeSharedImage(): Promise<{ if (!capacitor || !capacitor.Plugins) { logger.debug("[Main] Capacitor plugins not available"); + isProcessingSharedImage = false; return { success: false }; } @@ -117,6 +132,19 @@ async function checkAndStoreNativeSharedImage(): Promise<{ ); const platformService = PlatformServiceFactory.getInstance(); + // Clear old image from temp DB first to ensure we get the new one + try { + await platformService.dbExec("DELETE FROM temp WHERE id = ?", [ + SHARED_PHOTO_BASE64_KEY, + ]); + logger.debug("[Main] Cleared old shared image from temp DB"); + } catch (clearError) { + logger.debug( + "[Main] No old image to clear (or error clearing):", + clearError, + ); + } + // Convert raw base64 to data URL format that base64ToBlob expects // base64ToBlob expects format: "data:image/jpeg;base64,/9j/4AAQ..." // Try to detect image type from base64 or default to jpeg @@ -133,17 +161,19 @@ async function checkAndStoreNativeSharedImage(): Promise<{ [SHARED_PHOTO_BASE64_KEY, dataUrl], ); - // Delete the temp file + // Delete the temp file immediately after reading to prevent re-reading try { await Filesystem.deleteFile({ path: tempFilePath, directory: Directory.Documents, }); + logger.debug("[Main] Deleted temp file after reading"); } catch (deleteError) { logger.error("[Main] Failed to delete temp file:", deleteError); } logger.info(`[Main] Stored shared image: ${fileName}`); + isProcessingSharedImage = false; return { success: true, fileName }; } } @@ -175,6 +205,7 @@ async function checkAndStoreNativeSharedImage(): Promise<{ result = await shareImagePlugin.getSharedImageData(); } catch (pluginError) { logger.error("[Main] Plugin call failed:", pluginError); + isProcessingSharedImage = false; return { success: false }; } } else { @@ -206,12 +237,14 @@ async function checkAndStoreNativeSharedImage(): Promise<{ } } catch (executeError) { logger.error("[Main] Execute method failed:", executeError); + isProcessingSharedImage = false; return { success: false }; } } if (!result || !result.success || !result.data) { logger.debug("[Main] No shared image data found in result"); + isProcessingSharedImage = false; return { success: false }; } @@ -219,6 +252,7 @@ async function checkAndStoreNativeSharedImage(): Promise<{ if (!base64) { logger.debug("[Main] Shared image data missing base64"); + isProcessingSharedImage = false; return { success: false }; } @@ -226,18 +260,39 @@ async function checkAndStoreNativeSharedImage(): Promise<{ // Store in temp database (similar to web flow) const platformService = PlatformServiceFactory.getInstance(); - // $insertEntity is available via PlatformServiceMixin - // eslint-disable-next-line @typescript-eslint/no-explicit-any - await (platformService as any).$insertEntity( - "temp", - { id: SHARED_PHOTO_BASE64_KEY, blobB64: base64 }, - ["id", "blobB64"], + + // Clear old image from temp DB first to ensure we get the new one + try { + await platformService.dbExec("DELETE FROM temp WHERE id = ?", [ + SHARED_PHOTO_BASE64_KEY, + ]); + logger.debug("[Main] Cleared old shared image from temp DB"); + } catch (clearError) { + logger.debug( + "[Main] No old image to clear (or error clearing):", + clearError, + ); + } + + // Convert raw base64 to data URL format that base64ToBlob expects + let mimeType = "image/jpeg"; // default + if (base64.startsWith("/9j/") || base64.startsWith("iVBORw0KGgo")) { + mimeType = base64.startsWith("/9j/") ? "image/jpeg" : "image/png"; + } + const dataUrl = `data:${mimeType};base64,${base64}`; + + // Use INSERT OR REPLACE to handle existing records + await platformService.dbExec( + "INSERT OR REPLACE INTO temp (id, blobB64) VALUES (?, ?)", + [SHARED_PHOTO_BASE64_KEY, dataUrl], ); logger.info(`[Main] Stored shared image: ${fileName || "unknown"}`); + isProcessingSharedImage = false; return { success: true, fileName: fileName || "shared-image.jpg" }; } catch (error) { logger.error("[Main] Error checking for native shared image:", error); + isProcessingSharedImage = false; return { success: false }; } } @@ -247,41 +302,74 @@ const handleDeepLink = async (data: { url: string }) => { logger.debug(`[Main] 🌐 Deeplink received from Capacitor: ${url}`); try { - // Check if this is a shared-photo deep link from native share - const isSharedPhotoLink = url.includes("timesafari://shared-photo"); + // Handle empty path URLs from share extension (timesafari://) + // These are used to open the app, and we should check for shared images + const isEmptyPathUrl = url === "timesafari://" || url === "timesafari:///"; if ( - isSharedPhotoLink && + isEmptyPathUrl && Capacitor.isNativePlatform() && Capacitor.getPlatform() === "ios" ) { logger.debug( - "[Main] 📸 Shared photo deep link detected, checking for native shared image", + "[Main] 📸 Empty path URL from share extension, checking for native shared image", ); // Try to get shared image from App Group and store in temp database + // AppDelegate writes the file when the deep link is received, so we may need to retry try { - const imageResult = await checkAndStoreNativeSharedImage(); + let imageResult = await checkAndStoreNativeSharedImage(); + + // If not found immediately, wait a bit and retry (AppDelegate may still be writing the file) + if (!imageResult.success) { + logger.debug( + "[Main] ⏳ Image not found immediately, waiting for AppDelegate to write file...", + ); + await new Promise((resolve) => setTimeout(resolve, 300)); // Wait 300ms + imageResult = await checkAndStoreNativeSharedImage(); + } if (imageResult.success) { - logger.info("[Main] ✅ Native shared image stored in temp database"); + logger.info( + "[Main] ✅ Native shared image found, navigating to shared-photo", + ); - // Add fileName to the URL as a query parameter if we have it - if (imageResult.fileName) { - const urlObj = new URL(url); - urlObj.searchParams.set("fileName", imageResult.fileName); - const modifiedUrl = urlObj.toString(); - data.url = modifiedUrl; - logger.debug(`[Main] Added fileName to URL: ${modifiedUrl}`); + // Wait for router to be ready + await router.isReady(); + + // Navigate directly to shared-photo route + // Use replace if already on shared-photo to force refresh, otherwise push + const fileName = imageResult.fileName || "shared-image.jpg"; + const isAlreadyOnSharedPhoto = + router.currentRoute.value.path === "/shared-photo"; + + if (isAlreadyOnSharedPhoto) { + // Force refresh by replacing the route + await router.replace({ + path: "/shared-photo", + query: { fileName, _refresh: Date.now().toString() }, // Add timestamp to force update + }); + } else { + await router.push({ + path: "/shared-photo", + query: { fileName }, + }); } + + logger.info( + `[Main] ✅ Navigated to /shared-photo?fileName=${fileName}`, + ); + return; // Exit early, don't process as deep link } else { logger.debug( - "[Main] ℹ️ No native shared image found (may be from web or already processed)", + "[Main] ℹ️ No native shared image found, ignoring empty path URL", ); + return; // Exit early, don't process empty path as deep link } } catch (error) { logger.error("[Main] Error processing native shared image:", error); - // Continue with normal deep link processing even if native check fails + // If check fails, don't process as deep link (empty path would fail validation anyway) + return; } } @@ -372,10 +460,76 @@ const registerDeepLinkListener = async () => { } }; +/** + * Check for shared image and navigate to shared-photo route if found + * This is called when app becomes active (from share extension or app launch) + */ +async function checkForSharedImageAndNavigate() { + if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== "ios") { + return; + } + + try { + logger.debug("[Main] 🔍 Checking for shared image on app activation"); + const imageResult = await checkAndStoreNativeSharedImage(); + + if (imageResult.success) { + logger.info( + "[Main] ✅ Shared image found, navigating to shared-photo route", + ); + + // Wait for router to be ready + await router.isReady(); + + // Navigate to shared-photo route with fileName if available + // Use replace if already on shared-photo to force refresh, otherwise push + const fileName = imageResult.fileName || "shared-image.jpg"; + const isAlreadyOnSharedPhoto = + router.currentRoute.value.path === "/shared-photo"; + + if (isAlreadyOnSharedPhoto) { + // Force refresh by replacing the route + await router.replace({ + path: "/shared-photo", + query: { fileName, _refresh: Date.now().toString() }, // Add timestamp to force update + }); + } else { + const route = imageResult.fileName + ? `/shared-photo?fileName=${encodeURIComponent(imageResult.fileName)}` + : "/shared-photo"; + await router.push(route); + } + + logger.info(`[Main] ✅ Navigated to /shared-photo?fileName=${fileName}`); + } else { + logger.debug("[Main] ℹ️ No shared image found"); + } + } catch (error) { + logger.error("[Main] ❌ Error checking for shared image:", error); + } +} + logger.log("[Capacitor] 🚀 Mounting app"); app.mount("#app"); logger.info(`[Main] ✅ App mounted successfully`); +// Check for shared image on initial load (in case app was launched from share extension) +if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === "ios") { + setTimeout(async () => { + await checkForSharedImageAndNavigate(); + }, 1000); // Small delay to ensure router is ready +} + +// Listen for app state changes to detect when app becomes active +if (Capacitor.isNativePlatform() && Capacitor.getPlatform() === "ios") { + CapacitorApp.addListener("appStateChange", async ({ isActive }) => { + if (isActive) { + logger.debug("[Main] 📱 App became active, checking for shared image"); + await checkForSharedImageAndNavigate(); + } + }); +} + // Register deeplink listener after app is mounted setTimeout(async () => { try { diff --git a/src/views/SharedPhotoView.vue b/src/views/SharedPhotoView.vue index db1e8453..e9a33751 100644 --- a/src/views/SharedPhotoView.vue +++ b/src/views/SharedPhotoView.vue @@ -120,7 +120,7 @@