diff --git a/doc/ios-share-extension-git-commit-guide.md b/doc/ios-share-extension-git-commit-guide.md
new file mode 100644
index 0000000000..a0652d457f
--- /dev/null
+++ b/doc/ios-share-extension-git-commit-guide.md
@@ -0,0 +1,139 @@
+# iOS Share Extension - Git Commit Guide
+
+**Date:** 2025-01-27
+**Purpose:** Clarify which Xcode manual changes should be committed to the repository
+
+## Quick Answer
+
+**YES, most manual Xcode changes SHOULD be committed.** The only exceptions are user-specific settings that are already gitignored.
+
+## What Gets Modified (and Should Be Committed)
+
+When you create the Share Extension target and configure App Groups in Xcode, the following files are modified:
+
+### 1. `ios/App/App.xcodeproj/project.pbxproj` ✅ **COMMIT THIS**
+
+This is the main Xcode project file that tracks:
+- **New targets** (Share Extension target)
+- **File references** (which files belong to which targets)
+- **Build settings** (compiler flags, deployment targets, etc.)
+- **Build phases** (compile sources, link frameworks, etc.)
+- **Capabilities** (App Groups configuration)
+- **Target dependencies**
+
+**This file IS tracked in git** (not in `.gitignore`), so changes should be committed.
+
+### 2. Entitlements Files ✅ **COMMIT THESE**
+
+When you enable App Groups capability, Xcode creates/modifies:
+- `ios/App/App/App.entitlements` (for main app)
+- `ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements` (for extension)
+
+These files contain the App Group identifiers and should be committed.
+
+### 3. Share Extension Source Files ✅ **ALREADY COMMITTED**
+
+The following files are already in the repo:
+- `ios/App/TimeSafariShareExtension/ShareViewController.swift`
+- `ios/App/TimeSafariShareExtension/Info.plist`
+- `ios/App/App/ShareImageBridge.swift`
+
+These should already be committed (they were created as part of the implementation).
+
+## What Should NOT Be Committed
+
+### 1. User-Specific Settings ❌ **ALREADY GITIGNORED**
+
+These are in `ios/.gitignore`:
+- `xcuserdata/` - User-specific scheme selections, breakpoints, etc.
+- `*.xcuserstate` - User's current Xcode state
+
+### 2. Signing Identities ❌ **USER-SPECIFIC**
+
+While the **App Groups capability** should be committed (it's in `project.pbxproj` and entitlements), your **personal signing identity/team** is user-specific and Xcode handles this automatically per developer.
+
+## What Happens When You Commit
+
+When you commit the changes:
+
+1. **Other developers** who pull the changes will:
+ - ✅ Get the new Share Extension target automatically
+ - ✅ Get the App Groups capability configuration
+ - ✅ Get file references and build settings
+ - ✅ See the Share Extension in their Xcode project
+
+2. **They will still need to:**
+ - Configure their own signing team/identity (Xcode prompts for this)
+ - Build the project (which may trigger CocoaPods updates)
+ - But they **won't** need to manually create the target or configure App Groups
+
+## Step-by-Step: What to Commit
+
+After completing the Xcode setup steps:
+
+```bash
+# Check what changed
+git status
+
+# You should see:
+# - ios/App/App.xcodeproj/project.pbxproj (modified)
+# - ios/App/App/App.entitlements (new or modified)
+# - ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements (new)
+# - Possibly other project-related files
+
+# Review the changes
+git diff ios/App/App.xcodeproj/project.pbxproj
+
+# Commit the changes
+git add ios/App/App.xcodeproj/project.pbxproj
+git add ios/App/App/App.entitlements
+git add ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements
+git commit -m "Add iOS Share Extension target and App Groups configuration"
+```
+
+## Important Notes
+
+### Merge Conflicts in project.pbxproj
+
+The `project.pbxproj` file can have merge conflicts because:
+- It's auto-generated by Xcode
+- Multiple developers might modify it
+- It uses UUIDs that can conflict
+
+**If you get merge conflicts:**
+1. Open the project in Xcode
+2. Xcode will often auto-resolve conflicts
+3. Or manually resolve by keeping both sets of changes
+4. Test that the project builds
+
+### Team/Developer IDs
+
+The `DEVELOPMENT_TEAM` setting in `project.pbxproj` might be user-specific:
+- Some teams commit this (if everyone uses the same team)
+- Some teams use `.xcconfig` files to override per developer
+- Check with your team's practices
+
+If you see `DEVELOPMENT_TEAM = GM3FS5JQPH;` in the project file, this is already committed, so your team likely commits team IDs.
+
+## Verification
+
+After committing, verify that:
+1. The Share Extension target appears in Xcode for other developers
+2. App Groups capability is configured
+3. The project builds successfully
+4. No user-specific files were accidentally committed
+
+## Summary
+
+| Change Type | Commit? | Reason |
+|------------|---------|--------|
+| New target creation | ✅ Yes | Modifies `project.pbxproj` |
+| App Groups capability | ✅ Yes | Creates/modifies entitlements files |
+| File target membership | ✅ Yes | Modifies `project.pbxproj` |
+| Build settings | ✅ Yes | Modifies `project.pbxproj` |
+| Source files (Swift, plist) | ✅ Yes | Already in repo |
+| User scheme selections | ❌ No | In `xcuserdata/` (gitignored) |
+| Personal signing identity | ⚠️ Maybe | Depends on team practice |
+
+**Bottom line:** Commit all the Xcode project configuration changes. Other developers will get the Share Extension target automatically when they pull, and they'll only need to configure their personal signing settings.
+
diff --git a/doc/ios-share-extension-setup.md b/doc/ios-share-extension-setup.md
new file mode 100644
index 0000000000..83f49007a3
--- /dev/null
+++ b/doc/ios-share-extension-setup.md
@@ -0,0 +1,140 @@
+# iOS Share Extension Setup Instructions
+
+**Date:** 2025-01-27
+**Purpose:** Step-by-step instructions for setting up the iOS Share Extension in Xcode
+
+## Prerequisites
+
+- Xcode installed
+- iOS project already set up with Capacitor
+- Access to Apple Developer account (for App Groups)
+
+## Step 1: Create Share Extension Target
+
+1. Open `ios/App/App.xcodeproj` in Xcode
+2. In the Project Navigator, select the **App** project (top-level item)
+3. Click the **+** button at the bottom of the Targets list
+4. Select **iOS** → **Share Extension**
+5. Click **Next**
+6. Configure:
+ - **Product Name:** `TimeSafariShareExtension`
+ - **Bundle Identifier:** `app.timesafari.shareextension` (must match main app's bundle ID with `.shareextension` suffix)
+ - **Language:** Swift
+7. Click **Finish**
+
+## Step 2: Configure Share Extension Files
+
+The following files have been created in `ios/App/TimeSafariShareExtension/`:
+
+- `ShareViewController.swift` - Main extension logic
+- `Info.plist` - Extension configuration
+
+**Verify these files exist and are added to the Share Extension target.**
+
+## Step 3: Configure App Groups
+
+App Groups allow the Share Extension and main app to share data.
+
+### For Main App Target:
+
+1. Select the **App** target in Xcode
+2. Go to **Signing & Capabilities** tab
+3. Click **+ Capability**
+4. Select **App Groups**
+5. Click **+** to add a new group
+6. Enter: `group.app.timesafari`
+7. Ensure it's checked/enabled
+
+### For Share Extension Target:
+
+1. Select the **TimeSafariShareExtension** target
+2. Go to **Signing & Capabilities** tab
+3. Click **+ Capability**
+4. Select **App Groups**
+5. Click **+** to add a new group
+6. Enter: `group.app.timesafari` (same as main app)
+7. Ensure it's checked/enabled
+
+**Important:** Both targets must use the **exact same** App Group identifier.
+
+## Step 4: Configure Share Extension Info.plist
+
+The `Info.plist` file should already be configured, but verify:
+
+1. Select `TimeSafariShareExtension/Info.plist` in Xcode
+2. Ensure it contains:
+ - `NSExtensionPointIdentifier` = `com.apple.share-services`
+ - `NSExtensionPrincipalClass` = `$(PRODUCT_MODULE_NAME).ShareViewController`
+ - `NSExtensionActivationSupportsImageWithMaxCount` = `1`
+
+## Step 5: Add ShareImageBridge to Main App
+
+1. The file `ios/App/App/ShareImageBridge.swift` has been created
+2. Ensure it's added to the **App** target (not the Share Extension target)
+3. In Xcode, select the file and check the **Target Membership** in the File Inspector
+
+## Step 6: Build and Test
+
+1. Select the **App** scheme (not the Share Extension scheme)
+2. Build and run on a device or simulator
+3. Open Photos app
+4. Select an image
+5. Tap **Share** button
+6. Look for **TimeSafari Share** in the share sheet
+7. Select it
+8. The app should open and navigate to the shared photo view
+
+## Step 7: Troubleshooting
+
+### Share Extension doesn't appear in share sheet
+
+- Verify the Share Extension target builds successfully
+- Check that `Info.plist` is correctly configured
+- Ensure the extension's bundle identifier follows the pattern: `{main-app-bundle-id}.shareextension`
+- Clean build folder (Product → Clean Build Folder)
+
+### App Group access fails
+
+- Verify both targets have the same App Group identifier
+- Check that App Groups capability is enabled for both targets
+- Ensure you're signed in with a valid Apple Developer account
+- For development, you may need to enable App Groups in your Apple Developer account
+
+### Shared image not appearing
+
+- Check Xcode console for errors
+- Verify `ShareViewController.swift` is correctly implemented
+- Ensure the deep link `timesafari://shared-photo` is being handled
+- Check that the native bridge method is being called
+
+### Build errors
+
+- Ensure Swift version matches between targets
+- Check that all required frameworks are linked
+- Verify deployment targets match between main app and extension
+
+## Step 8: Native Bridge Implementation (TODO)
+
+Currently, the JavaScript code needs a way to call the native `getSharedImageData()` method. This requires one of:
+
+1. **Option A:** Create a minimal Capacitor plugin
+2. **Option B:** Use Capacitor's existing bridge mechanisms
+3. **Option C:** Expose the method via a custom URL scheme parameter
+
+The current implementation in `main.capacitor.ts` has a placeholder that needs to be completed.
+
+## Next Steps
+
+After the Share Extension is set up and working:
+
+1. Complete the native bridge implementation to read from App Group
+2. Test end-to-end flow: Share image → Extension stores → App reads → Displays
+3. Implement Android version
+4. Add error handling and edge cases
+
+## References
+
+- [Apple Share Extensions Documentation](https://developer.apple.com/documentation/social)
+- [App Groups Documentation](https://developer.apple.com/documentation/xcode/configuring-app-groups)
+- [Capacitor Native Bridge](https://capacitorjs.com/docs/guides/building-plugins)
+
diff --git a/doc/ios-share-implementation-status.md b/doc/ios-share-implementation-status.md
new file mode 100644
index 0000000000..2b182b7bd9
--- /dev/null
+++ b/doc/ios-share-implementation-status.md
@@ -0,0 +1,93 @@
+# iOS Share Extension Implementation Status
+
+**Date:** 2025-01-27
+**Status:** In Progress - Native Code Complete, Bridge Pending
+
+## Completed
+
+✅ **Share Extension Files Created:**
+- `ios/App/TimeSafariShareExtension/ShareViewController.swift` - Handles image sharing
+- `ios/App/TimeSafariShareExtension/Info.plist` - Extension configuration
+
+✅ **Native Bridge Created:**
+- `ios/App/App/ShareImageBridge.swift` - Native method to read from App Group
+
+✅ **JavaScript Integration Started:**
+- `src/services/nativeShareHandler.ts` - Service to handle native shared images
+- `src/main.capacitor.ts` - Updated to check for native shared images on deep link
+
+✅ **Documentation:**
+- `doc/native-share-target-implementation.md` - Complete implementation guide
+- `doc/ios-share-extension-setup.md` - Xcode setup instructions
+
+## Pending
+
+⚠️ **Xcode Configuration (Manual Steps Required):**
+1. Create Share Extension target in Xcode
+2. Configure App Groups for both main app and extension
+3. Add ShareImageBridge.swift to App target
+4. Build and test
+
+⚠️ **JavaScript-Native Bridge:**
+The current implementation has a placeholder for calling the native `ShareImageBridge.getSharedImageData()` method from JavaScript. This needs to be completed using one of:
+
+**Option A: Minimal Capacitor Plugin** (Recommended for Option 1)
+- Create a small plugin that exposes the method
+- Clean and maintainable
+- Follows Capacitor patterns
+
+**Option B: Direct Bridge Call**
+- Use Capacitor's executePlugin or similar mechanism
+- Requires understanding Capacitor's internal bridge
+- Less maintainable
+
+**Option C: AppDelegate Integration**
+- Have AppDelegate check on launch and expose via a different mechanism
+- Workaround approach
+- Less clean but functional
+
+## Next Steps
+
+1. **Complete Xcode Setup:**
+ - Follow `doc/ios-share-extension-setup.md`
+ - Create Share Extension target
+ - Configure App Groups
+ - Build and verify extension appears in share sheet
+
+2. **Implement JavaScript-Native Bridge:**
+ - Choose one of the options above
+ - Complete the `checkAndStoreNativeSharedImage()` function in `main.capacitor.ts`
+ - Test end-to-end flow
+
+3. **Testing:**
+ - Share image from Photos app
+ - Verify Share Extension appears
+ - Verify app opens and displays shared image
+ - Test "Record Gift" and "Save as Profile" flows
+
+## Current Flow
+
+1. ✅ User shares image → Share Extension receives
+2. ✅ Share Extension converts to base64
+3. ✅ Share Extension stores in App Group UserDefaults
+4. ✅ Share Extension opens app with `timesafari://shared-photo?fileName=...`
+5. ⚠️ App receives deep link (handled)
+6. ⚠️ App checks App Group UserDefaults (bridge needed)
+7. ⚠️ App stores in temp database (pending bridge)
+8. ✅ SharedPhotoView reads from temp database (already works)
+
+## Code Locations
+
+- **Share Extension:** `ios/App/TimeSafariShareExtension/`
+- **Native Bridge:** `ios/App/App/ShareImageBridge.swift`
+- **JavaScript Handler:** `src/services/nativeShareHandler.ts`
+- **Deep Link Integration:** `src/main.capacitor.ts`
+- **View Component:** `src/views/SharedPhotoView.vue` (already complete)
+
+## Notes
+
+- The Share Extension code is complete and ready to use
+- The main missing piece is the JavaScript-to-native bridge
+- Once the bridge is complete, the entire flow should work end-to-end
+- The existing `SharedPhotoView.vue` doesn't need changes - it already handles images from temp storage
+
diff --git a/doc/native-share-target-implementation.md b/doc/native-share-target-implementation.md
new file mode 100644
index 0000000000..7a30979c37
--- /dev/null
+++ b/doc/native-share-target-implementation.md
@@ -0,0 +1,507 @@
+# Native Share Target Implementation Guide
+
+**Date:** 2025-01-27
+**Purpose:** Enable TimeSafari native iOS and Android apps to receive shared images from other apps
+
+## Current State
+
+The app currently supports **PWA/web share target** functionality:
+- Service worker intercepts POST to `/share-target`
+- Images stored in temp database as base64
+- `SharedPhotoView.vue` processes and displays shared images
+
+**This does NOT work for native iOS/Android builds** because:
+- Service workers don't run in native app contexts
+- Native platforms use different sharing mechanisms (Share Extensions on iOS, Intent Filters on Android)
+
+## Required Changes
+
+### 1. iOS Implementation
+
+#### 1.1 Create Share Extension Target
+
+1. Open `ios/App/App.xcodeproj` in Xcode
+2. File → New → Target
+3. Select "Share Extension" template
+4. Name it "TimeSafariShareExtension"
+5. Bundle Identifier: `app.timesafari.shareextension`
+6. Language: Swift
+
+#### 1.2 Configure Share Extension Info.plist
+
+Add to `ios/App/TimeSafariShareExtension/Info.plist`:
+
+```xml
+NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.share-services
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).ShareViewController
+ NSExtensionActivationRule
+
+ NSExtensionActivationSupportsImageWithMaxCount
+ 1
+ NSExtensionActivationSupportsFileWithMaxCount
+ 1
+
+
+```
+
+#### 1.3 Implement ShareViewController
+
+Create `ios/App/TimeSafariShareExtension/ShareViewController.swift`:
+
+```swift
+import UIKit
+import Social
+import MobileCoreServices
+import Capacitor
+
+class ShareViewController: SLComposeServiceViewController {
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ self.title = "Share to TimeSafari"
+ }
+
+ override func isContentValid() -> Bool {
+ return true
+ }
+
+ override func didSelectPost() {
+ guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
+ let itemProvider = extensionItem.attachments?.first else {
+ self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
+ return
+ }
+
+ // Handle image sharing
+ if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
+ itemProvider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil) { [weak self] (item, error) in
+ guard let self = self else { return }
+
+ if let url = item as? URL {
+ // Handle file URL
+ self.handleSharedImage(url: url)
+ } else if let image = item as? UIImage {
+ // Handle UIImage directly
+ self.handleSharedImage(image: image)
+ } else if let data = item as? Data {
+ // Handle image data
+ self.handleSharedImage(data: data)
+ }
+ }
+ }
+ }
+
+ private func handleSharedImage(url: URL? = nil, image: UIImage? = nil, data: Data? = nil) {
+ var imageData: Data?
+ var fileName: String?
+
+ if let url = url {
+ imageData = try? Data(contentsOf: url)
+ fileName = url.lastPathComponent
+ } else if let image = image {
+ imageData = image.jpegData(compressionQuality: 0.8)
+ fileName = "shared-image.jpg"
+ } else if let data = data {
+ imageData = data
+ fileName = "shared-image.jpg"
+ }
+
+ guard let imageData = imageData else {
+ self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
+ return
+ }
+
+ // Convert to base64
+ let base64String = imageData.base64EncodedString()
+
+ // Store in shared UserDefaults (accessible by main app)
+ let userDefaults = UserDefaults(suiteName: "group.app.timesafari")
+ userDefaults?.set(base64String, forKey: "sharedPhotoBase64")
+ userDefaults?.set(fileName ?? "shared-image.jpg", forKey: "sharedPhotoFileName")
+ userDefaults?.synchronize()
+
+ // Open main app with deep link
+ let url = URL(string: "timesafari://shared-photo?fileName=\(fileName?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "shared-image.jpg")")!
+ var responder = self as UIResponder?
+ while responder != nil {
+ if let application = responder as? UIApplication {
+ application.open(url, options: [:], completionHandler: nil)
+ break
+ }
+ responder = responder?.next
+ }
+
+ // Close share extension
+ self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
+ }
+
+ override func configurationItems() -> [Any]! {
+ return []
+ }
+}
+```
+
+#### 1.4 Configure App Groups
+
+1. In Xcode, select main app target → Signing & Capabilities
+2. Add "App Groups" capability
+3. Create group: `group.app.timesafari`
+4. Repeat for Share Extension target with same group name
+
+#### 1.5 Update Main App to Read from App Group
+
+The main app needs to check for shared images on launch. This should be added to `AppDelegate.swift` or handled in JavaScript.
+
+### 2. Android Implementation
+
+#### 2.1 Update AndroidManifest.xml
+
+Add intent filter to `MainActivity` in `android/app/src/main/AndroidManifest.xml`:
+
+```xml
+
+
+ ... existing intent filters ...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+#### 2.2 Handle Intent in MainActivity
+
+Update `android/app/src/main/java/app/timesafari/MainActivity.java`:
+
+```java
+package app.timesafari;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Base64;
+import android.util.Log;
+import com.getcapacitor.BridgeActivity;
+import com.getcapacitor.Plugin;
+import java.io.InputStream;
+import java.io.ByteArrayOutputStream;
+
+public class MainActivity extends BridgeActivity {
+ private static final String TAG = "MainActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ handleShareIntent(getIntent());
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ setIntent(intent);
+ handleShareIntent(intent);
+ }
+
+ private void handleShareIntent(Intent intent) {
+ if (intent == null) return;
+
+ String action = intent.getAction();
+ String type = intent.getType();
+
+ if (Intent.ACTION_SEND.equals(action) && type != null && type.startsWith("image/")) {
+ Uri imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (imageUri != null) {
+ handleSharedImage(imageUri, intent.getStringExtra(Intent.EXTRA_TEXT));
+ }
+ } else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null && type.startsWith("image/")) {
+ // Handle multiple images (optional - for now just take first)
+ java.util.ArrayList imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ if (imageUris != null && !imageUris.isEmpty()) {
+ handleSharedImage(imageUris.get(0), null);
+ }
+ }
+ }
+
+ private void handleSharedImage(Uri imageUri, String fileName) {
+ try {
+ // Read image data
+ InputStream inputStream = getContentResolver().openInputStream(imageUri);
+ if (inputStream == null) {
+ Log.e(TAG, "Failed to open input stream for shared image");
+ return;
+ }
+
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ byte[] data = new byte[8192];
+ int nRead;
+ while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
+ buffer.write(data, 0, nRead);
+ }
+ buffer.flush();
+ byte[] imageBytes = buffer.toByteArray();
+
+ // Convert to base64
+ String base64String = Base64.encodeToString(imageBytes, Base64.NO_WRAP);
+
+ // Extract filename from URI or use default
+ String actualFileName = fileName;
+ if (actualFileName == null || actualFileName.isEmpty()) {
+ String path = imageUri.getPath();
+ if (path != null) {
+ int lastSlash = path.lastIndexOf('/');
+ if (lastSlash >= 0 && lastSlash < path.length() - 1) {
+ actualFileName = path.substring(lastSlash + 1);
+ }
+ }
+ if (actualFileName == null || actualFileName.isEmpty()) {
+ actualFileName = "shared-image.jpg";
+ }
+ }
+
+ // Store in SharedPreferences (accessible by JavaScript via Capacitor)
+ android.content.SharedPreferences prefs = getSharedPreferences("TimeSafariShared", MODE_PRIVATE);
+ android.content.SharedPreferences.Editor editor = prefs.edit();
+ editor.putString("sharedPhotoBase64", base64String);
+ editor.putString("sharedPhotoFileName", actualFileName);
+ editor.apply();
+
+ // Trigger JavaScript event or navigate to shared-photo route
+ // This will be handled by JavaScript checking for shared data on app launch
+ Log.d(TAG, "Shared image stored, filename: " + actualFileName);
+
+ } catch (Exception e) {
+ Log.e(TAG, "Error handling shared image", e);
+ }
+ }
+}
+```
+
+#### 2.3 Add Required Permissions
+
+Ensure `AndroidManifest.xml` has:
+
+```xml
+
+
+```
+
+### 3. JavaScript Layer Updates
+
+#### 3.1 Create Native Share Handler
+
+Create `src/services/nativeShareHandler.ts`:
+
+```typescript
+/**
+ * Native Share Handler
+ * Handles shared images from native iOS and Android platforms
+ */
+
+import { Capacitor } from "@capacitor/core";
+import { App } from "@capacitor/app";
+import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
+import { logger } from "../utils/logger";
+import { SHARED_PHOTO_BASE64_KEY } from "../libs/util";
+import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
+
+/**
+ * Check for shared images from native platforms and store in temp database
+ */
+export async function checkForNativeSharedImage(
+ platformService: InstanceType
+): Promise {
+ if (!Capacitor.isNativePlatform()) {
+ return false;
+ }
+
+ try {
+ if (Capacitor.getPlatform() === "ios") {
+ return await checkIOSSharedImage(platformService);
+ } else if (Capacitor.getPlatform() === "android") {
+ return await checkAndroidSharedImage(platformService);
+ }
+ } catch (error) {
+ logger.error("Error checking for native shared image:", error);
+ }
+
+ return false;
+}
+
+/**
+ * Check for shared image on iOS (from App Group UserDefaults)
+ */
+async function checkIOSSharedImage(
+ platformService: InstanceType
+): Promise {
+ // iOS uses App Groups to share data between extension and main app
+ // We need to use a Capacitor plugin or native code to read from App Group
+ // For now, this is a placeholder - requires native plugin implementation
+
+ // Option 1: Use Capacitor plugin to read from App Group
+ // Option 2: Use native code bridge
+
+ logger.debug("Checking for iOS shared image (not yet implemented)");
+ return false;
+}
+
+/**
+ * Check for shared image on Android (from SharedPreferences)
+ */
+async function checkAndroidSharedImage(
+ platformService: InstanceType
+): Promise {
+ // Android stores in SharedPreferences
+ // We need a Capacitor plugin to read from SharedPreferences
+ // For now, this is a placeholder - requires native plugin implementation
+
+ logger.debug("Checking for Android shared image (not yet implemented)");
+ return false;
+}
+
+/**
+ * Store shared image in temp database
+ */
+async function storeSharedImage(
+ base64Data: string,
+ fileName: string,
+ platformService: InstanceType
+): Promise {
+ try {
+ const existing = await platformService.$getTemp(SHARED_PHOTO_BASE64_KEY);
+
+ if (existing) {
+ await platformService.$updateEntity(
+ "temp",
+ { blobB64: base64Data },
+ "id = ?",
+ [SHARED_PHOTO_BASE64_KEY]
+ );
+ } else {
+ await platformService.$insertEntity(
+ "temp",
+ { id: SHARED_PHOTO_BASE64_KEY, blobB64: base64Data },
+ ["id", "blobB64"]
+ );
+ }
+
+ logger.debug("Stored shared image in temp database");
+ } catch (error) {
+ logger.error("Error storing shared image:", error);
+ throw error;
+ }
+}
+```
+
+#### 3.2 Update main.capacitor.ts
+
+Add check for shared images on app launch:
+
+```typescript
+// In main.capacitor.ts, after app mount:
+
+import { checkForNativeSharedImage } from "./services/nativeShareHandler";
+
+// Check for shared images when app becomes active
+App.addListener("appStateChange", async (state) => {
+ if (state.isActive) {
+ // Check for native shared images
+ const hasSharedImage = await checkForNativeSharedImage(/* platformService */);
+ if (hasSharedImage) {
+ // Navigate to shared-photo view
+ await router.push({
+ name: "shared-photo",
+ query: { source: "native" }
+ });
+ }
+ }
+});
+
+// Also check on initial launch
+App.getLaunchUrl().then((result) => {
+ if (result?.url) {
+ // Handle deep link
+ } else {
+ // Check for shared image
+ checkForNativeSharedImage(/* platformService */).then((hasShared) => {
+ if (hasShared) {
+ router.push({ name: "shared-photo", query: { source: "native" } });
+ }
+ });
+ }
+});
+```
+
+#### 3.3 Update SharedPhotoView.vue
+
+The existing `SharedPhotoView.vue` should work as-is, but we may want to add detection for native vs web sources.
+
+### 4. Alternative Approach: Capacitor Plugin
+
+Instead of implementing native code directly, consider creating a Capacitor plugin:
+
+1. **Create plugin**: `@capacitor-community/share-target` or custom plugin
+2. **Plugin methods**:
+ - `checkForSharedImage()`: Returns shared image data if available
+ - `clearSharedImage()`: Clears shared image data after processing
+
+This would be cleaner and more maintainable.
+
+### 5. Testing Checklist
+
+- [ ] Test sharing image from Photos app on iOS
+- [ ] Test sharing image from Gallery app on Android
+- [ ] Test sharing from other apps (Safari, Chrome, etc.)
+- [ ] Verify image appears in SharedPhotoView
+- [ ] Test "Record Gift" flow with shared image
+- [ ] Test "Save as Profile" flow with shared image
+- [ ] Test cancel flow
+- [ ] Verify temp storage cleanup
+- [ ] Test app launch with shared image pending
+- [ ] Test app already running when image is shared
+
+### 6. Implementation Priority
+
+**Phase 1: Android (Simpler)**
+1. Update AndroidManifest.xml
+2. Implement MainActivity intent handling
+3. Create JavaScript handler
+4. Test end-to-end
+
+**Phase 2: iOS (More Complex)**
+1. Create Share Extension target
+2. Implement ShareViewController
+3. Configure App Groups
+4. Create JavaScript handler
+5. Test end-to-end
+
+### 7. Notes
+
+- **App Groups (iOS)**: Required for sharing data between Share Extension and main app
+- **SharedPreferences (Android)**: Standard way to share data between app components
+- **Base64 Encoding**: Both platforms convert images to base64 for JavaScript compatibility
+- **File Size Limits**: Consider large image handling and memory management
+- **Permissions**: Android 13+ requires `READ_MEDIA_IMAGES` instead of `READ_EXTERNAL_STORAGE`
+
+### 8. References
+
+- [iOS Share Extensions](https://developer.apple.com/documentation/social)
+- [Android Share Targets](https://developer.android.com/training/sharing/receive)
+- [Capacitor App Plugin](https://capacitorjs.com/docs/apis/app)
+- [Capacitor Native Bridge](https://capacitorjs.com/docs/guides/building-plugins)
+
diff --git a/doc/xcode-26-cocoapods-workaround.md b/doc/xcode-26-cocoapods-workaround.md
new file mode 100644
index 0000000000..2db40f4411
--- /dev/null
+++ b/doc/xcode-26-cocoapods-workaround.md
@@ -0,0 +1,76 @@
+# Xcode 26 / CocoaPods Compatibility Workaround
+
+**Date:** 2025-01-27
+**Issue:** CocoaPods `xcodeproj` gem (1.27.0) doesn't support Xcode 26's project format version 70
+
+## The Problem
+
+Xcode 26.1.1 uses project format version 70, but the `xcodeproj` gem (1.27.0) only supports up to version 56. This causes CocoaPods to fail with:
+
+```
+ArgumentError - [Xcodeproj] Unable to find compatibility version string for object version `70`.
+```
+
+## Solutions
+
+### Option 1: Temporarily Downgrade Project Format (Recommended for Now)
+
+**Before running `pod install` or `npm run build:ios`:**
+
+1. Edit `ios/App/App.xcodeproj/project.pbxproj`
+2. Change line 6 from: `objectVersion = 70;` to: `objectVersion = 56;`
+3. Run your build/sync command
+4. Change it back to: `objectVersion = 70;` (Xcode will likely change it back automatically)
+
+**Warning:** Xcode may automatically upgrade the format back to 70 when you open the project. This is okay - just repeat the process when needed.
+
+### Option 2: Wait for xcodeproj Update
+
+The `xcodeproj` gem maintainers will eventually release a version that supports format 70. You can:
+- Check for updates: `bundle update xcodeproj`
+- Monitor: https://github.com/CocoaPods/Xcodeproj/issues
+
+### Option 3: Use Xcode Directly (Bypass CocoaPods for Now)
+
+Since the Share Extension is already set up:
+1. Open the project in Xcode
+2. Build directly from Xcode (Product → Build)
+3. Skip `npm run build:ios` for now
+4. Test the Share Extension functionality
+
+### Option 4: Automated Workaround (Integrated into Build Script) ✅
+
+The workaround is now **automatically integrated** into `scripts/build-ios.sh`. When you run:
+
+```bash
+npm run build:ios
+```
+
+The build script will:
+1. Automatically detect if the project format is version 70
+2. Temporarily downgrade to version 56
+3. Run `pod install`
+4. Restore to version 70
+5. Continue with the build
+
+**No manual steps required!** The workaround is transparent and only applies when needed.
+
+To remove the workaround in the future:
+1. Check if `xcodeproj` gem supports format 70: `bundle exec gem list xcodeproj`
+2. Test if `pod install` works without the workaround
+3. If it works, remove the `run_pod_install_with_workaround()` function from `scripts/build-ios.sh`
+4. Replace it with a simple `pod install` call
+
+## Current Status
+
+- ✅ Share Extension target exists
+- ✅ Share Extension files are in place
+- ✅ Workaround integrated into build script
+- ✅ `npm run build:ios` works automatically
+
+## Recommendation
+
+**Use `npm run build:ios`** - the workaround is handled automatically. No manual intervention needed.
+
+Once `xcodeproj` is updated to support format 70, the workaround can be removed from the build script.
+
diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj
index ce46d1f6c5..7214d4a44c 100644
--- a/ios/App/App.xcodeproj/project.pbxproj
+++ b/ios/App/App.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 54;
+ objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -15,8 +15,35 @@
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */; };
+ C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+ C86585E82ED45A3E00824752 /* ShareImageBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = C86585E72ED45A3D00824752 /* ShareImageBridge.swift */; };
+ C8D7E2CC2ED46A3B00DD738D /* ShareImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D7E2CB2ED46A3B00DD738D /* ShareImagePlugin.swift */; };
/* End PBXBuildFile section */
+/* Begin PBXContainerItemProxy section */
+ C86585DD2ED456DE00824752 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 504EC2FC1FED79650016851F /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = C86585D42ED456DE00824752;
+ remoteInfo = TimeSafariShareExtension;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ C86585E02ED456DE00824752 /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ C86585DF2ED456DE00824752 /* TimeSafariShareExtension.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
/* Begin PBXFileReference section */
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; };
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; };
@@ -28,10 +55,28 @@
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; };
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TimeSafariShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ C86585E52ED4577F00824752 /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = ""; };
+ C86585E72ED45A3D00824752 /* ShareImageBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareImageBridge.swift; sourceTree = ""; };
+ C8D7E2CB2ED46A3B00DD738D /* ShareImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareImagePlugin.swift; sourceTree = ""; };
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; };
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
+/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
+ C86585E32ED456DE00824752 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
+ isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
+ membershipExceptions = (
+ Info.plist,
+ );
+ target = C86585D42ED456DE00824752 /* TimeSafariShareExtension */;
+ };
+/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
+
+/* Begin PBXFileSystemSynchronizedRootGroup section */
+ C86585D62ED456DE00824752 /* TimeSafariShareExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C86585E32ED456DE00824752 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = TimeSafariShareExtension; sourceTree = ""; };
+/* End PBXFileSystemSynchronizedRootGroup section */
+
/* Begin PBXFrameworksBuildPhase section */
504EC3011FED79650016851F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
@@ -41,6 +86,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ C86585D22ED456DE00824752 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -56,6 +108,7 @@
isa = PBXGroup;
children = (
504EC3061FED79650016851F /* App */,
+ C86585D62ED456DE00824752 /* TimeSafariShareExtension */,
504EC3051FED79650016851F /* Products */,
BA325FFCDCE8D334E5C7AEBE /* Pods */,
4B546315E668C7A13939F417 /* Frameworks */,
@@ -66,6 +119,7 @@
isa = PBXGroup;
children = (
504EC3041FED79650016851F /* App.app */,
+ C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */,
);
name = Products;
sourceTree = "";
@@ -73,6 +127,9 @@
504EC3061FED79650016851F /* App */ = {
isa = PBXGroup;
children = (
+ C8D7E2CB2ED46A3B00DD738D /* ShareImagePlugin.swift */,
+ C86585E72ED45A3D00824752 /* ShareImageBridge.swift */,
+ C86585E52ED4577F00824752 /* App.entitlements */,
50379B222058CBB4000EE86E /* capacitor.config.json */,
504EC3071FED79650016851F /* AppDelegate.swift */,
504EC30B1FED79650016851F /* Main.storyboard */,
@@ -108,16 +165,40 @@
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
+ C86585E02ED456DE00824752 /* Embed Foundation Extensions */,
);
buildRules = (
);
dependencies = (
+ C86585DE2ED456DE00824752 /* PBXTargetDependency */,
);
name = App;
productName = App;
productReference = 504EC3041FED79650016851F /* App.app */;
productType = "com.apple.product-type.application";
};
+ C86585D42ED456DE00824752 /* TimeSafariShareExtension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = C86585E42ED456DE00824752 /* Build configuration list for PBXNativeTarget "TimeSafariShareExtension" */;
+ buildPhases = (
+ C86585D12ED456DE00824752 /* Sources */,
+ C86585D22ED456DE00824752 /* Frameworks */,
+ C86585D32ED456DE00824752 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ fileSystemSynchronizedGroups = (
+ C86585D62ED456DE00824752 /* TimeSafariShareExtension */,
+ );
+ name = TimeSafariShareExtension;
+ packageProductDependencies = (
+ );
+ productName = TimeSafariShareExtension;
+ productReference = C86585D52ED456DE00824752 /* TimeSafariShareExtension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -125,7 +206,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
- LastSwiftUpdateCheck = 920;
+ LastSwiftUpdateCheck = 2610;
LastUpgradeCheck = 1630;
TargetAttributes = {
504EC3031FED79650016851F = {
@@ -133,6 +214,9 @@
LastSwiftMigration = 1100;
ProvisioningStyle = Automatic;
};
+ C86585D42ED456DE00824752 = {
+ CreatedOnToolsVersion = 26.1.1;
+ };
};
};
buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
@@ -149,6 +233,7 @@
projectRoot = "";
targets = (
504EC3031FED79650016851F /* App */,
+ C86585D42ED456DE00824752 /* TimeSafariShareExtension */,
);
};
/* End PBXProject section */
@@ -167,6 +252,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ C86585D32ED456DE00824752 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -253,12 +345,29 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ C8D7E2CC2ED46A3B00DD738D /* ShareImagePlugin.swift in Sources */,
+ C86585E82ED45A3E00824752 /* ShareImageBridge.swift in Sources */,
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+ C86585D12ED456DE00824752 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
+/* Begin PBXTargetDependency section */
+ C86585DE2ED456DE00824752 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = C86585D42ED456DE00824752 /* TimeSafariShareExtension */;
+ targetProxy = C86585DD2ED456DE00824752 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
/* Begin PBXVariantGroup section */
504EC30B1FED79650016851F /* Main.storyboard */ = {
isa = PBXVariantGroup;
@@ -402,6 +511,7 @@
baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = GM3FS5JQPH;
@@ -429,6 +539,7 @@
baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 48;
DEVELOPMENT_TEAM = GM3FS5JQPH;
@@ -450,6 +561,80 @@
};
name = Release;
};
+ C86585E12ED456DE00824752 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = GM3FS5JQPH;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = TimeSafariShareExtension;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 26.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ C86585E22ED456DE00824752 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CODE_SIGN_ENTITLEMENTS = TimeSafariShareExtension/TimeSafariShareExtension.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = GM3FS5JQPH;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = TimeSafariShareExtension/Info.plist;
+ INFOPLIST_KEY_CFBundleDisplayName = TimeSafariShareExtension;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ IPHONEOS_DEPLOYMENT_TARGET = 26.1;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@executable_path/../../Frameworks",
+ );
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MARKETING_VERSION = 1.0;
+ MTL_FAST_MATH = YES;
+ PRODUCT_BUNDLE_IDENTIFIER = app.timesafari.TimeSafariShareExtension;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
+ SWIFT_APPROACHABLE_CONCURRENCY = YES;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -471,6 +656,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ C86585E42ED456DE00824752 /* Build configuration list for PBXNativeTarget "TimeSafariShareExtension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ C86585E12ED456DE00824752 /* Debug */,
+ C86585E22ED456DE00824752 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
};
rootObject = 504EC2FC1FED79650016851F /* Project object */;
diff --git a/ios/App/App/App.entitlements b/ios/App/App/App.entitlements
new file mode 100644
index 0000000000..68633259f5
--- /dev/null
+++ b/ios/App/App/App.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.app.timesafari
+
+
+
diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift
index 7a1b41b323..33937d3fad 100644
--- a/ios/App/App/AppDelegate.swift
+++ b/ios/App/App/AppDelegate.swift
@@ -39,6 +39,33 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
+ // Check if this is a shared-photo deep link and store image data in a way JS can access
+ if url.scheme == "timesafari" && url.host == "shared-photo" {
+ // Try to get shared image from App Group and store it in a temp file that JS can read
+ // This is a workaround until the plugin is properly registered
+ if let sharedData = getSharedImageData() {
+ // Write to a temp file in the app's Documents directory that JavaScript can read via Filesystem plugin
+ let fileManager = FileManager.default
+ if let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
+ let tempFileURL = documentsDir.appendingPathComponent("timesafari_shared_photo.json")
+
+ // Create JSON data
+ let jsonData: [String: String] = [
+ "base64": sharedData["base64"] ?? "",
+ "fileName": sharedData["fileName"] ?? ""
+ ]
+
+ if let json = try? JSONSerialization.data(withJSONObject: jsonData, options: []) {
+ do {
+ try json.write(to: tempFileURL)
+ } catch {
+ // Error writing temp file - will be handled by JS layer
+ }
+ }
+ }
+ }
+ }
+
// Called when the app was launched with a url. Feel free to add additional processing here,
// but if you want the App API to support tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
@@ -50,5 +77,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// tracking app url opens, make sure to keep this call
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
}
+
+ /**
+ * Check for shared image from Share Extension
+ * Reads from App Group UserDefaults and returns shared image data if available
+ *
+ * @returns Dictionary with "base64" and "fileName" keys, or nil if no shared image
+ */
+ func getSharedImageData() -> [String: String]? {
+ let appGroupIdentifier = "group.app.timesafari"
+ guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
+ return nil
+ }
+
+ guard let base64 = userDefaults.string(forKey: "sharedPhotoBase64"),
+ let fileName = userDefaults.string(forKey: "sharedPhotoFileName") else {
+ return nil
+ }
+
+ // Clear the shared data after reading
+ userDefaults.removeObject(forKey: "sharedPhotoBase64")
+ userDefaults.removeObject(forKey: "sharedPhotoFileName")
+ userDefaults.synchronize()
+
+ return ["base64": base64, "fileName": fileName]
+ }
}
diff --git a/ios/App/App/ShareImageBridge.swift b/ios/App/App/ShareImageBridge.swift
new file mode 100644
index 0000000000..88ad874295
--- /dev/null
+++ b/ios/App/App/ShareImageBridge.swift
@@ -0,0 +1,48 @@
+import Foundation
+
+/**
+ * Share Image Bridge
+ *
+ * Provides a bridge between JavaScript and native iOS code to access
+ * shared images stored in App Group UserDefaults by the Share Extension.
+ *
+ * This bridge allows the JavaScript layer to read shared image data
+ * that was stored by the Share Extension.
+ *
+ * Note: This class doesn't need Capacitor - it's a simple Swift utility
+ * that reads from App Group UserDefaults. The JavaScript bridge will be
+ * implemented separately.
+ */
+@objc(ShareImageBridge)
+public class ShareImageBridge: NSObject {
+
+ private static let appGroupIdentifier = "group.app.timesafari"
+ private static let sharedPhotoBase64Key = "sharedPhotoBase64"
+ private static let sharedPhotoFileNameKey = "sharedPhotoFileName"
+
+ /**
+ * Get shared image data from App Group UserDefaults
+ * Called from JavaScript via Capacitor bridge
+ *
+ * @returns Dictionary with "base64" and "fileName" keys, or nil if no shared image
+ */
+ @objc public static func getSharedImageData() -> [String: String]? {
+ guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
+ print("ShareImageBridge: Failed to access App Group UserDefaults")
+ return nil
+ }
+
+ guard let base64 = userDefaults.string(forKey: sharedPhotoBase64Key),
+ let fileName = userDefaults.string(forKey: sharedPhotoFileNameKey) else {
+ return nil
+ }
+
+ // Clear the shared data after reading
+ userDefaults.removeObject(forKey: sharedPhotoBase64Key)
+ userDefaults.removeObject(forKey: sharedPhotoFileNameKey)
+ userDefaults.synchronize()
+
+ return ["base64": base64, "fileName": fileName]
+ }
+}
+
diff --git a/ios/App/App/ShareImagePlugin.swift b/ios/App/App/ShareImagePlugin.swift
new file mode 100644
index 0000000000..9073de7f6e
--- /dev/null
+++ b/ios/App/App/ShareImagePlugin.swift
@@ -0,0 +1,28 @@
+import Foundation
+import Capacitor
+
+/**
+ * Share Image Plugin
+ *
+ * Capacitor plugin that exposes ShareImageBridge functionality to JavaScript.
+ * Allows JavaScript to retrieve shared images from App Group UserDefaults.
+ */
+@objc(ShareImagePlugin)
+public class ShareImagePlugin: CAPPlugin {
+
+ @objc func getSharedImageData(_ call: CAPPluginCall) {
+ guard let sharedData = ShareImageBridge.getSharedImageData() else {
+ call.resolve(["success": false, "data": NSNull()])
+ return
+ }
+
+ call.resolve([
+ "success": true,
+ "data": [
+ "base64": sharedData["base64"] ?? "",
+ "fileName": sharedData["fileName"] ?? ""
+ ]
+ ])
+ }
+}
+
diff --git a/ios/App/TimeSafariShareExtension/Info.plist b/ios/App/TimeSafariShareExtension/Info.plist
new file mode 100644
index 0000000000..9230ac2404
--- /dev/null
+++ b/ios/App/TimeSafariShareExtension/Info.plist
@@ -0,0 +1,21 @@
+
+
+
+
+ NSExtension
+
+ NSExtensionAttributes
+
+ NSExtensionActivationRule
+
+ NSExtensionActivationSupportsImageWithMaxCount
+ 1
+
+
+ NSExtensionPointIdentifier
+ com.apple.share-services
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).ShareViewController
+
+
+
diff --git a/ios/App/TimeSafariShareExtension/ShareViewController.swift b/ios/App/TimeSafariShareExtension/ShareViewController.swift
new file mode 100644
index 0000000000..9090d128ea
--- /dev/null
+++ b/ios/App/TimeSafariShareExtension/ShareViewController.swift
@@ -0,0 +1,170 @@
+//
+// ShareViewController.swift
+// TimeSafariShareExtension
+//
+// Created by Aardimus on 11/24/25.
+//
+
+import UIKit
+import Social
+import UniformTypeIdentifiers
+
+class ShareViewController: SLComposeServiceViewController {
+
+ private let appGroupIdentifier = "group.app.timesafari"
+ private let sharedPhotoBase64Key = "sharedPhotoBase64"
+ private let sharedPhotoFileNameKey = "sharedPhotoFileName"
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ // Set placeholder text (required for SLComposeServiceViewController)
+ self.placeholder = "Share image to TimeSafari"
+
+ // Validate content on load
+ self.validateContent()
+ }
+
+ 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 {
+ 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)
+ return
+ }
+
+ if success {
+ // Open the main app via deep link
+ self.openMainApp()
+ }
+
+ // Complete the extension context
+ extensionContext.completeRequest(returningItems: [], completionHandler: nil)
+ }
+ }
+
+ private func processSharedImage(from items: [NSExtensionItem], completion: @escaping (Bool) -> Void) {
+ // Find the first image attachment
+ for item in items {
+ guard let attachments = item.attachments else {
+ continue
+ }
+
+ for attachment in attachments {
+ if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
+ attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
+ guard let self = self else {
+ completion(false)
+ return
+ }
+
+ if let error = error {
+ completion(false)
+ return
+ }
+
+ // Handle different image data types
+ var imageData: Data?
+ var fileName: String = "shared-image.jpg"
+
+ if let url = data as? URL {
+ // Image provided as file URL
+ imageData = try? Data(contentsOf: url)
+ fileName = url.lastPathComponent
+ } else if let image = data as? UIImage {
+ // Image provided as UIImage
+ imageData = image.jpegData(compressionQuality: 0.9)
+ fileName = "shared-image.jpg"
+ } else if let data = data as? Data {
+ // Image provided as raw Data
+ imageData = data
+ fileName = "shared-image.jpg"
+ }
+
+ guard let finalImageData = imageData else {
+ completion(false)
+ return
+ }
+
+ // Convert to base64
+ let base64String = finalImageData.base64EncodedString()
+
+ // Store in App Group UserDefaults
+ guard let userDefaults = UserDefaults(suiteName: self.appGroupIdentifier) else {
+ completion(false)
+ return
+ }
+
+ userDefaults.set(base64String, forKey: self.sharedPhotoBase64Key)
+ userDefaults.set(fileName, forKey: self.sharedPhotoFileNameKey)
+ userDefaults.synchronize()
+
+ completion(true)
+ }
+ return // Process only the first image
+ }
+ }
+ }
+
+ // No image found
+ completion(false)
+ }
+
+ private func openMainApp() {
+ // Open the main app via deep link
+ 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
+ }
+
+ // Fallback: use extension context
+ extensionContext?.open(url, completionHandler: nil)
+ }
+
+ override func configurationItems() -> [Any]! {
+ // No additional configuration options needed
+ return []
+ }
+
+}
diff --git a/ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements b/ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements
new file mode 100644
index 0000000000..68633259f5
--- /dev/null
+++ b/ios/App/TimeSafariShareExtension/TimeSafariShareExtension.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.app.timesafari
+
+
+
diff --git a/scripts/build-ios.sh b/scripts/build-ios.sh
index b327152c6c..fde5ee7d0c 100755
--- a/scripts/build-ios.sh
+++ b/scripts/build-ios.sh
@@ -404,8 +404,141 @@ elif [ "$BUILD_MODE" = "production" ]; then
safe_execute "Building Capacitor version (production)" "npm run build:capacitor -- --mode production" || exit 3
fi
-# Step 6: Sync with Capacitor
-safe_execute "Syncing with Capacitor" "npx cap sync ios" || exit 6
+# Step 6: Install CocoaPods dependencies (with Xcode 26 workaround)
+# ===================================================================
+# WORKAROUND: Xcode 26 / CocoaPods Compatibility Issue
+# ===================================================================
+# Xcode 26 uses project format version 70, but CocoaPods' xcodeproj gem
+# (1.27.0) only supports up to version 56. This causes pod install to fail.
+#
+# This workaround temporarily downgrades the project format to 56, runs
+# pod install, then restores it to 70. Xcode will automatically upgrade
+# it back to 70 when opened, which is fine.
+#
+# NOTE: Both explicit pod install AND Capacitor sync (which runs pod install
+# internally) need this workaround. See run_pod_install_with_workaround()
+# and run_cap_sync_with_workaround() functions below.
+#
+# TO REMOVE THIS WORKAROUND IN THE FUTURE:
+# 1. Check if xcodeproj gem has been updated: bundle exec gem list xcodeproj
+# 2. Test if pod install works without the workaround
+# 3. If it works, remove both workaround functions below
+# 4. Replace with:
+# - safe_execute "Installing CocoaPods dependencies" "cd ios/App && bundle exec pod install && cd ../.." || exit 6
+# - safe_execute "Syncing with Capacitor" "npx cap sync ios" || exit 6
+# 5. Update this comment to indicate the workaround has been removed
+# ===================================================================
+run_pod_install_with_workaround() {
+ local PROJECT_FILE="ios/App/App.xcodeproj/project.pbxproj"
+
+ log_info "Installing CocoaPods dependencies (with Xcode 26 workaround)..."
+
+ # Check if project file exists
+ if [ ! -f "$PROJECT_FILE" ]; then
+ log_error "Project file not found: $PROJECT_FILE"
+ return 1
+ fi
+
+ # Check current format version
+ local current_version=$(grep "objectVersion" "$PROJECT_FILE" | head -1 | grep -o "[0-9]\+" || echo "")
+
+ if [ -z "$current_version" ]; then
+ log_error "Could not determine project format version"
+ return 1
+ fi
+
+ log_debug "Current project format version: $current_version"
+
+ # Only apply workaround if format is 70
+ if [ "$current_version" = "70" ]; then
+ log_debug "Applying Xcode 26 workaround: temporarily downgrading to format 56"
+
+ # Downgrade to format 56 (supported by CocoaPods)
+ if ! sed -i '' 's/objectVersion = 70;/objectVersion = 56;/' "$PROJECT_FILE"; then
+ log_error "Failed to downgrade project format"
+ return 1
+ fi
+
+ # Run pod install
+ log_info "Running pod install..."
+ if ! (cd ios/App && bundle exec pod install && cd ../..); then
+ log_error "pod install failed"
+ # Try to restore format even on failure
+ sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE" || true
+ return 1
+ fi
+
+ # Restore to format 70
+ log_debug "Restoring project format to 70..."
+ if ! sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE"; then
+ log_warn "Failed to restore project format to 70 (Xcode will upgrade it automatically)"
+ fi
+
+ log_success "CocoaPods dependencies installed successfully"
+ else
+ # Format is not 70, run pod install normally
+ log_debug "Project format is $current_version, running pod install normally"
+ if ! (cd ios/App && bundle exec pod install && cd ../..); then
+ log_error "pod install failed"
+ return 1
+ fi
+ log_success "CocoaPods dependencies installed successfully"
+ fi
+}
+
+safe_execute "Installing CocoaPods dependencies" "run_pod_install_with_workaround" || exit 6
+
+# Step 6.5: Sync with Capacitor (also needs workaround since it runs pod install internally)
+# Capacitor sync internally runs pod install, so we need to apply the workaround here too
+run_cap_sync_with_workaround() {
+ local PROJECT_FILE="ios/App/App.xcodeproj/project.pbxproj"
+
+ # Check current format version
+ local current_version=$(grep "objectVersion" "$PROJECT_FILE" | head -1 | grep -o "[0-9]\+" || echo "")
+
+ if [ -z "$current_version" ]; then
+ log_error "Could not determine project format version for Capacitor sync"
+ return 1
+ fi
+
+ # Only apply workaround if format is 70
+ if [ "$current_version" = "70" ]; then
+ log_debug "Applying Xcode 26 workaround for Capacitor sync: temporarily downgrading to format 56"
+
+ # Downgrade to format 56 (supported by CocoaPods)
+ if ! sed -i '' 's/objectVersion = 70;/objectVersion = 56;/' "$PROJECT_FILE"; then
+ log_error "Failed to downgrade project format for Capacitor sync"
+ return 1
+ fi
+
+ # Run Capacitor sync (which will run pod install internally)
+ log_info "Running Capacitor sync..."
+ if ! npx cap sync ios; then
+ log_error "Capacitor sync failed"
+ # Try to restore format even on failure
+ sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE" || true
+ return 1
+ fi
+
+ # Restore to format 70
+ log_debug "Restoring project format to 70 after Capacitor sync..."
+ if ! sed -i '' 's/objectVersion = 56;/objectVersion = 70;/' "$PROJECT_FILE"; then
+ log_warn "Failed to restore project format to 70 (Xcode will upgrade it automatically)"
+ fi
+
+ log_success "Capacitor sync completed successfully"
+ else
+ # Format is not 70, run sync normally
+ log_debug "Project format is $current_version, running Capacitor sync normally"
+ if ! npx cap sync ios; then
+ log_error "Capacitor sync failed"
+ return 1
+ fi
+ log_success "Capacitor sync completed successfully"
+ fi
+}
+
+safe_execute "Syncing with Capacitor" "run_cap_sync_with_workaround" || exit 6
# Step 7: Generate assets
safe_execute "Generating assets" "npx capacitor-assets generate --ios" || exit 7
diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts
index 0fe5c68dd9..60c0826ce0 100644
--- a/src/interfaces/deepLinks.ts
+++ b/src/interfaces/deepLinks.ts
@@ -68,6 +68,7 @@ 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 f091770b1c..3ef253a546 100644
--- a/src/main.capacitor.ts
+++ b/src/main.capacitor.ts
@@ -30,11 +30,15 @@
import { initializeApp } from "./main.common";
import { App as CapacitorApp } from "@capacitor/app";
+import { Capacitor } from "@capacitor/core";
+import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
import router from "./router";
import { handleApiError } from "./services/api";
import { AxiosError } from "axios";
import { DeepLinkHandler } from "./services/deepLinks";
import { logger, safeStringify } from "./utils/logger";
+import { PlatformServiceFactory } from "./services/PlatformServiceFactory";
+import { SHARED_PHOTO_BASE64_KEY } from "./libs/util";
import "./utils/safeAreaInset";
logger.log("[Capacitor] 🚀 Starting initialization");
@@ -67,11 +71,220 @@ const deepLinkHandler = new DeepLinkHandler(router);
*
* @throws {Error} If URL format is invalid
*/
+/**
+ * Check for native shared image from iOS App Group UserDefaults
+ * and store in temp database before routing to shared-photo view
+ */
+async function checkAndStoreNativeSharedImage(): Promise<{
+ success: boolean;
+ fileName?: string;
+}> {
+ if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== "ios") {
+ return { success: false };
+ }
+
+ try {
+ logger.debug("[Main] Checking for iOS shared image from App Group");
+
+ // Use Capacitor's native bridge to call the ShareImagePlugin
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const capacitor = (window as any).Capacitor;
+
+ if (!capacitor || !capacitor.Plugins) {
+ logger.debug("[Main] Capacitor plugins not available");
+ return { success: false };
+ }
+
+ // WORKAROUND: Since the plugin isn't being auto-discovered, use a temp file bridge
+ // AppDelegate writes the shared image data to a temp file, and we read it here
+ try {
+ const tempFilePath = "timesafari_shared_photo.json";
+ const fileContent = await Filesystem.readFile({
+ path: tempFilePath,
+ directory: Directory.Documents,
+ encoding: Encoding.UTF8,
+ });
+
+ if (fileContent.data) {
+ const sharedData = JSON.parse(fileContent.data as string);
+ const base64 = sharedData.base64;
+ const fileName = sharedData.fileName || "shared-image.jpg";
+
+ if (base64) {
+ // Store in temp database using dbExec directly
+ logger.info(
+ "[Main] Native shared image found (via temp file), storing in temp DB",
+ );
+ const platformService = PlatformServiceFactory.getInstance();
+
+ // 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
+ let mimeType = "image/jpeg"; // default
+ if (base64.startsWith("/9j/") || base64.startsWith("iVBORw0KGgo")) {
+ // JPEG or PNG
+ 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],
+ );
+
+ // Delete the temp file
+ try {
+ await Filesystem.deleteFile({
+ path: tempFilePath,
+ directory: Directory.Documents,
+ });
+ } catch (deleteError) {
+ logger.error("[Main] Failed to delete temp file:", deleteError);
+ }
+
+ logger.info(`[Main] Stored shared image: ${fileName}`);
+ return { success: true, fileName };
+ }
+ }
+ } catch (fileError: unknown) {
+ // File doesn't exist or can't be read - that's okay, try plugin method
+ logger.debug(
+ "[Main] Temp file not found or unreadable (this is normal if plugin works)",
+ );
+ }
+
+ // NOTE: Plugin registration issue - ShareImage plugin is not being auto-discovered
+ // This is a known issue that needs to be resolved. For now, we use the temp file workaround above.
+
+ // Try multiple methods to call the plugin (fallback if temp file method fails)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const plugins = (capacitor as any).Plugins;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const shareImagePlugin = (plugins as any)?.ShareImage;
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let result: any = null;
+
+ if (
+ shareImagePlugin &&
+ typeof shareImagePlugin.getSharedImageData === "function"
+ ) {
+ logger.debug("[Main] Using direct plugin method");
+ try {
+ result = await shareImagePlugin.getSharedImageData();
+ } catch (pluginError) {
+ logger.error("[Main] Plugin call failed:", pluginError);
+ return { success: false };
+ }
+ } else {
+ // Method 2: Use Capacitor's execute method
+ logger.debug(
+ "[Main] Plugin not found directly, trying Capacitor.execute",
+ );
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const bridge = (capacitor as any).getBridge?.();
+ if (bridge) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ result = await bridge.execute({
+ pluginId: "ShareImage",
+ methodName: "getSharedImageData",
+ options: {},
+ });
+ } else {
+ // Method 3: Try execute on Plugins object
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if ((plugins as any)?.execute) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ result = await (plugins as any).execute(
+ "ShareImage",
+ "getSharedImageData",
+ {},
+ );
+ }
+ }
+ } catch (executeError) {
+ logger.error("[Main] Execute method failed:", executeError);
+ return { success: false };
+ }
+ }
+
+ if (!result || !result.success || !result.data) {
+ logger.debug("[Main] No shared image data found in result");
+ return { success: false };
+ }
+
+ const { base64, fileName } = result.data;
+
+ if (!base64) {
+ logger.debug("[Main] Shared image data missing base64");
+ return { success: false };
+ }
+
+ logger.info("[Main] Native shared image found, storing in temp DB");
+
+ // 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"],
+ );
+
+ logger.info(`[Main] Stored shared image: ${fileName || "unknown"}`);
+ return { success: true, fileName: fileName || "shared-image.jpg" };
+ } catch (error) {
+ logger.error("[Main] Error checking for native shared image:", error);
+ return { success: false };
+ }
+}
+
const handleDeepLink = async (data: { url: string }) => {
const { url } = data;
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");
+
+ if (
+ isSharedPhotoLink &&
+ Capacitor.isNativePlatform() &&
+ Capacitor.getPlatform() === "ios"
+ ) {
+ logger.debug(
+ "[Main] 📸 Shared photo deep link detected, checking for native shared image",
+ );
+
+ // Try to get shared image from App Group and store in temp database
+ try {
+ const imageResult = await checkAndStoreNativeSharedImage();
+
+ if (imageResult.success) {
+ logger.info("[Main] ✅ Native shared image stored in temp database");
+
+ // 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}`);
+ }
+ } else {
+ logger.debug(
+ "[Main] ℹ️ No native shared image found (may be from web or already processed)",
+ );
+ }
+ } catch (error) {
+ logger.error("[Main] Error processing native shared image:", error);
+ // Continue with normal deep link processing even if native check fails
+ }
+ }
+
// Wait for router to be ready
logger.debug(`[Main] ⏳ Waiting for router to be ready...`);
await router.isReady();