# 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
<key>NSExtension</key>
<dict>
    <key>NSExtensionPointIdentifier</key>
    <string>com.apple.share-services</string>
    <key>NSExtensionPrincipalClass</key>
    <string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
    <key>NSExtensionActivationRule</key>
    <dict>
        <key>NSExtensionActivationSupportsImageWithMaxCount</key>
        <integer>1</integer>
        <key>NSExtensionActivationSupportsFileWithMaxCount</key>
        <integer>1</integer>
    </dict>
</dict>
```

#### 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.share")
        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.share`
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
<activity
    android:name=".MainActivity"
    ... existing attributes ...>
    
    ... existing intent filters ...
    
    <!-- Share Target Intent Filter -->
    <intent-filter>
        <action android:name="android.intent.action.SEND" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="image/*" />
    </intent-filter>
    
    <!-- Multiple images support (optional) -->
    <intent-filter>
        <action android:name="android.intent.action.SEND_MULTIPLE" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="image/*" />
    </intent-filter>
</activity>
```

#### 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<Uri> 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
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <!-- Android 13+ -->
```

### 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<typeof PlatformServiceMixin>
): Promise<boolean> {
  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<typeof PlatformServiceMixin>
): Promise<boolean> {
  // 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<typeof PlatformServiceMixin>
): Promise<boolean> {
  // 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<typeof PlatformServiceMixin>
): Promise<void> {
  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)

