Compare commits
14 Commits
homeview-g
...
web-share-
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d2201fc17 | |||
|
|
bb92e3ac4f | ||
|
|
a672c977a8 | ||
|
|
0c66142093 | ||
|
|
84983ee10b | ||
|
|
eeac7fdb66 | ||
|
|
1a8383bc63 | ||
|
|
4c771d8be3 | ||
|
|
0627cd32b7 | ||
|
|
e1eb91f26d | ||
|
|
09a230f43e | ||
|
|
eff4126043 | ||
|
|
ae49c0e907 | ||
|
|
1b4ab7a500 |
@@ -27,6 +27,20 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="timesafari" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Share Target Intent Filter - Single Image -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Share Target Intent Filter - Multiple Images (optional, we'll handle first image) -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
|
||||
@@ -34,5 +34,13 @@
|
||||
{
|
||||
"pkg": "@capawesome/capacitor-file-picker",
|
||||
"classpath": "io.capawesome.capacitorjs.plugins.filepicker.FilePickerPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "SafeArea",
|
||||
"classpath": "app.timesafari.safearea.SafeAreaPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "SharedImage",
|
||||
"classpath": "app.timesafari.sharedimage.SharedImagePlugin"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package app.timesafari;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.WindowInsetsController;
|
||||
@@ -11,9 +15,21 @@ import android.webkit.WebSettings;
|
||||
import android.webkit.WebViewClient;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
import app.timesafari.safearea.SafeAreaPlugin;
|
||||
import app.timesafari.sharedimage.SharedImagePlugin;
|
||||
//import com.getcapacitor.community.sqlite.SQLite;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import java.io.InputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
private static final String TAG = "MainActivity";
|
||||
private static final String SHARED_PREFS_NAME = "shared_image";
|
||||
private static final String KEY_BASE64 = "shared_image_base64";
|
||||
private static final String KEY_FILE_NAME = "shared_image_file_name";
|
||||
private static final String KEY_READY = "shared_image_ready";
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -48,9 +64,156 @@ public class MainActivity extends BridgeActivity {
|
||||
// Register SafeArea plugin
|
||||
registerPlugin(SafeAreaPlugin.class);
|
||||
|
||||
// Register SharedImage plugin
|
||||
registerPlugin(SharedImagePlugin.class);
|
||||
|
||||
// Initialize SQLite
|
||||
//registerPlugin(SQLite.class);
|
||||
|
||||
// Handle share intent if app was launched from share sheet
|
||||
handleShareIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
handleShareIntent(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle share intents (ACTION_SEND or ACTION_SEND_MULTIPLE)
|
||||
* Processes shared images and stores them in SharedPreferences for plugin to read
|
||||
*/
|
||||
private void handleShareIntent(Intent intent) {
|
||||
if (intent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String action = intent.getAction();
|
||||
String type = intent.getType();
|
||||
|
||||
boolean handled = false;
|
||||
|
||||
// Handle single image share
|
||||
if (Intent.ACTION_SEND.equals(action) && type != null && type.startsWith("image/")) {
|
||||
Uri imageUri;
|
||||
// Use new API for API 33+ (Android 13+), fall back to deprecated API for older versions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri.class);
|
||||
} else {
|
||||
// Deprecated but still works on older versions
|
||||
@SuppressWarnings("deprecation")
|
||||
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
imageUri = uri;
|
||||
}
|
||||
if (imageUri != null) {
|
||||
String fileName = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
processSharedImage(imageUri, fileName);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
// Handle multiple images share (we'll just process the first one)
|
||||
else if (Intent.ACTION_SEND_MULTIPLE.equals(action) && type != null && type.startsWith("image/")) {
|
||||
java.util.ArrayList<Uri> imageUris;
|
||||
// Use new API for API 33+ (Android 13+), fall back to deprecated API for older versions
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
imageUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri.class);
|
||||
} else {
|
||||
// Deprecated but still works on older versions
|
||||
@SuppressWarnings("deprecation")
|
||||
java.util.ArrayList<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
imageUris = uris;
|
||||
}
|
||||
if (imageUris != null && !imageUris.isEmpty()) {
|
||||
processSharedImage(imageUris.get(0), null);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the intent after handling to release URI permissions and prevent
|
||||
// network issues in WebView. This is critical for preventing the WebView
|
||||
// from losing network connectivity after processing shared content.
|
||||
if (handled) {
|
||||
intent.setAction(null);
|
||||
intent.setData(null);
|
||||
intent.removeExtra(Intent.EXTRA_STREAM);
|
||||
intent.setType(null);
|
||||
setIntent(new Intent());
|
||||
Log.d(TAG, "Cleared share intent after processing");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a shared image: read it, convert to base64, and write to temp file
|
||||
* Uses try-with-resources to ensure proper stream cleanup and prevent network issues
|
||||
*/
|
||||
private void processSharedImage(Uri imageUri, String fileName) {
|
||||
// Extract filename from URI or use default (do this before opening streams)
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
// Use try-with-resources to ensure streams are properly closed
|
||||
// This is critical to prevent resource leaks that can affect WebView networking
|
||||
try (InputStream inputStream = getContentResolver().openInputStream(imageUri);
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {
|
||||
|
||||
if (inputStream == null) {
|
||||
Log.e(TAG, "Failed to open input stream for shared image");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read image bytes
|
||||
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);
|
||||
|
||||
// Store in SharedPreferences for plugin to read
|
||||
storeSharedImageInPreferences(base64String, actualFileName);
|
||||
|
||||
Log.d(TAG, "Successfully processed shared image: " + actualFileName);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error processing shared image", e);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Unexpected error processing shared image", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store shared image data in SharedPreferences for plugin to read
|
||||
* Plugin will read and clear the data when called
|
||||
*/
|
||||
private void storeSharedImageInPreferences(String base64, String fileName) {
|
||||
try {
|
||||
SharedPreferences prefs = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putString(KEY_BASE64, base64);
|
||||
editor.putString(KEY_FILE_NAME, fileName);
|
||||
editor.putBoolean(KEY_READY, true);
|
||||
editor.apply();
|
||||
|
||||
Log.d(TAG, "Stored shared image data in SharedPreferences");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Error storing shared image in SharedPreferences", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package app.timesafari.sharedimage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
@CapacitorPlugin(name = "SharedImage")
|
||||
public class SharedImagePlugin extends Plugin {
|
||||
|
||||
private static final String SHARED_PREFS_NAME = "shared_image";
|
||||
private static final String KEY_BASE64 = "shared_image_base64";
|
||||
private static final String KEY_FILE_NAME = "shared_image_file_name";
|
||||
private static final String KEY_READY = "shared_image_ready";
|
||||
|
||||
/**
|
||||
* Get shared image data from SharedPreferences
|
||||
* Returns base64 string and fileName, or null if no image exists
|
||||
* Clears the data after reading to prevent re-reading
|
||||
*/
|
||||
@PluginMethod
|
||||
public void getSharedImage(PluginCall call) {
|
||||
try {
|
||||
SharedPreferences prefs = getSharedPreferences();
|
||||
|
||||
String base64 = prefs.getString(KEY_BASE64, null);
|
||||
String fileName = prefs.getString(KEY_FILE_NAME, null);
|
||||
|
||||
if (base64 == null || fileName == null) {
|
||||
// No shared image exists - return null values (not an error)
|
||||
JSObject result = new JSObject();
|
||||
result.put("base64", (String) null);
|
||||
result.put("fileName", (String) null);
|
||||
call.resolve(result);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear the shared data after reading
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.remove(KEY_BASE64);
|
||||
editor.remove(KEY_FILE_NAME);
|
||||
editor.remove(KEY_READY);
|
||||
editor.apply();
|
||||
|
||||
// Return the shared image data
|
||||
JSObject result = new JSObject();
|
||||
result.put("base64", base64);
|
||||
result.put("fileName", fileName);
|
||||
call.resolve(result);
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("SharedImagePlugin", "Error in getSharedImage()", e);
|
||||
call.reject("Error getting shared image: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shared image exists without reading it
|
||||
* Useful for quick checks before calling getSharedImage()
|
||||
*/
|
||||
@PluginMethod
|
||||
public void hasSharedImage(PluginCall call) {
|
||||
SharedPreferences prefs = getSharedPreferences();
|
||||
boolean hasImage = prefs.contains(KEY_BASE64) && prefs.contains(KEY_FILE_NAME);
|
||||
|
||||
JSObject result = new JSObject();
|
||||
result.put("hasImage", hasImage);
|
||||
call.resolve(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SharedPreferences instance for shared image data
|
||||
*/
|
||||
private SharedPreferences getSharedPreferences() {
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
throw new IllegalStateException("Plugin context is null");
|
||||
}
|
||||
return context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ext {
|
||||
minSdkVersion = 22
|
||||
minSdkVersion = 23
|
||||
compileSdkVersion = 36
|
||||
targetSdkVersion = 36
|
||||
androidxActivityVersion = '1.8.0'
|
||||
|
||||
259
doc/android-api-23-upgrade-impact-analysis.md
Normal file
259
doc/android-api-23-upgrade-impact-analysis.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Android API 23 Upgrade Impact Analysis
|
||||
|
||||
**Date:** 2025-12-03
|
||||
**Current minSdkVersion:** 22 (Android 5.1 Lollipop)
|
||||
**Proposed minSdkVersion:** 23 (Android 6.0 Marshmallow)
|
||||
**Impact Assessment:** Low to Moderate
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Upgrading from API 22 to API 23 will have **minimal code impact** but may affect device compatibility. The main change is that API 23 introduced runtime permissions, but since the app uses Capacitor plugins which handle permissions, the impact is minimal.
|
||||
|
||||
## Code Impact Analysis
|
||||
|
||||
### ✅ No Breaking Changes in Existing Code
|
||||
|
||||
#### 1. API Level Checks in Code
|
||||
All existing API level checks are for **much higher APIs** than 23, so they won't be affected:
|
||||
|
||||
**MainActivity.java:**
|
||||
- `Build.VERSION_CODES.R` (API 30+) - Edge-to-edge display
|
||||
- `Build.VERSION_CODES.TIRAMISU` (API 33+) - Intent extras handling
|
||||
- Legacy path (API 21-29) - Will still work, but API 22 devices won't be supported
|
||||
|
||||
**SafeAreaPlugin.java:**
|
||||
- `Build.VERSION_CODES.R` (API 30+) - Safe area insets
|
||||
|
||||
**Conclusion:** No code changes needed for API level checks.
|
||||
|
||||
#### 2. Permissions Handling
|
||||
|
||||
**Current Permissions in AndroidManifest.xml:**
|
||||
- `INTERNET` - Normal permission (no runtime needed)
|
||||
- `READ_EXTERNAL_STORAGE` - Dangerous permission (runtime required on API 23+)
|
||||
- `WRITE_EXTERNAL_STORAGE` - Dangerous permission (runtime required on API 23+)
|
||||
- `CAMERA` - Dangerous permission (runtime required on API 23+)
|
||||
|
||||
**Current Implementation:**
|
||||
- ✅ App uses **Capacitor plugins** for camera and file access
|
||||
- ✅ Capacitor plugins **already handle runtime permissions** automatically
|
||||
- ✅ No manual permission request code found in the codebase
|
||||
- ✅ QR Scanner uses Capacitor's BarcodeScanner plugin which handles permissions
|
||||
|
||||
**Conclusion:** No code changes needed - Capacitor handles runtime permissions automatically.
|
||||
|
||||
#### 3. Dependencies Compatibility
|
||||
|
||||
**AndroidX Libraries:**
|
||||
- `androidx.appcompat:appcompat:1.6.1` - ✅ Supports API 23+
|
||||
- `androidx.core:core:1.12.0` - ✅ Supports API 23+
|
||||
- `androidx.fragment:fragment:1.6.2` - ✅ Supports API 23+
|
||||
- `androidx.coordinatorlayout:coordinatorlayout:1.2.0` - ✅ Supports API 23+
|
||||
- `androidx.core:core-splashscreen:1.0.1` - ✅ Supports API 23+
|
||||
|
||||
**Capacitor Plugins:**
|
||||
- `@capacitor/core:6.2.0` - ✅ Requires API 23+ (official requirement)
|
||||
- `@capacitor/camera:6.0.0` - ✅ Handles runtime permissions
|
||||
- `@capacitor/filesystem:6.0.0` - ✅ Handles runtime permissions
|
||||
- `@capacitor-community/sqlite:6.0.2` - ✅ Supports API 23+
|
||||
- `@capacitor-mlkit/barcode-scanning:6.0.0` - ✅ Supports API 23+
|
||||
|
||||
**Third-Party Libraries:**
|
||||
- No Firebase or other libraries with API 22-specific requirements found
|
||||
- All dependencies appear compatible with API 23+
|
||||
|
||||
**Conclusion:** All dependencies are compatible with API 23.
|
||||
|
||||
#### 4. Build Configuration
|
||||
|
||||
**Current Configuration:**
|
||||
- `compileSdkVersion = 36` (Android 14)
|
||||
- `targetSdkVersion = 36` (Android 14)
|
||||
- `minSdkVersion = 22` (Android 5.1) ← **Only this needs to change**
|
||||
|
||||
**Required Change:**
|
||||
```gradle
|
||||
// android/variables.gradle
|
||||
ext {
|
||||
minSdkVersion = 23 // Change from 22 to 23
|
||||
// ... rest stays the same
|
||||
}
|
||||
```
|
||||
|
||||
**Conclusion:** Only one line needs to be changed.
|
||||
|
||||
## Device Compatibility Impact
|
||||
|
||||
### Device Coverage Loss
|
||||
|
||||
**API 22 (Android 5.1 Lollipop):**
|
||||
- Released: March 2015
|
||||
- Market share: ~0.1% of active devices (as of 2024)
|
||||
- Devices affected: Very old devices from 2015-2016
|
||||
|
||||
**API 23 (Android 6.0 Marshmallow):**
|
||||
- Released: October 2015
|
||||
- Market share: ~0.3% of active devices (as of 2024)
|
||||
- Still very low, but slightly higher than API 22
|
||||
|
||||
**Impact:** Losing support for ~0.1% of devices (essentially negligible)
|
||||
|
||||
### User Base Impact
|
||||
|
||||
**Recommendation:** Check your analytics to see actual usage:
|
||||
- If you have analytics, check percentage of users on API 22
|
||||
- If < 0.5%, upgrade is safe
|
||||
- If > 1%, consider the business impact
|
||||
|
||||
## Runtime Permissions (API 23 Feature)
|
||||
|
||||
### What Changed in API 23
|
||||
|
||||
**Before API 23 (API 22 and below):**
|
||||
- Permissions granted at install time
|
||||
- User sees all permissions during installation
|
||||
- No runtime permission dialogs
|
||||
|
||||
**API 23+ (Runtime Permissions):**
|
||||
- Dangerous permissions must be requested at runtime
|
||||
- User sees permission dialogs when app needs them
|
||||
- Better user experience and privacy
|
||||
|
||||
### Current App Status
|
||||
|
||||
**✅ Already Compatible:**
|
||||
- App uses Capacitor plugins which **automatically handle runtime permissions**
|
||||
- Camera plugin requests permissions when needed
|
||||
- Filesystem plugin requests permissions when needed
|
||||
- No manual permission code needed
|
||||
|
||||
**Conclusion:** App is already designed for runtime permissions via Capacitor.
|
||||
|
||||
## Potential Issues to Watch
|
||||
|
||||
### 1. APK Size
|
||||
- Some developers report APK size increases after raising minSdkVersion
|
||||
- **Action:** Monitor APK size after upgrade
|
||||
- **Expected Impact:** Minimal (API 22 → 23 is a small jump)
|
||||
|
||||
### 2. Testing Requirements
|
||||
- Need to test on API 23+ devices
|
||||
- **Action:** Test on Android 6.0+ devices/emulators
|
||||
- **Current:** App likely already tested on API 23+ devices
|
||||
|
||||
### 3. Legacy Code Path
|
||||
- MainActivity has legacy code for API 21-29
|
||||
- **Impact:** This code will still work, but API 22 devices won't be supported
|
||||
- **Action:** No code changes needed, but legacy path becomes API 23-29
|
||||
|
||||
### 4. Capacitor Compatibility
|
||||
- Capacitor 6.2.0 officially requires API 23+
|
||||
- **Current Situation:** App runs on API 22 (may be working due to leniency)
|
||||
- **After Upgrade:** Officially compliant with Capacitor requirements
|
||||
- **Benefit:** Better compatibility guarantees
|
||||
|
||||
## Files That Need Changes
|
||||
|
||||
### 1. Build Configuration
|
||||
**File:** `android/variables.gradle`
|
||||
```gradle
|
||||
ext {
|
||||
minSdkVersion = 23 // Change from 22
|
||||
// ... rest unchanged
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Documentation
|
||||
**Files to Update:**
|
||||
- `doc/shared-image-plugin-implementation-plan.md` - Update version notes
|
||||
- Any README files mentioning API 22
|
||||
- Build documentation
|
||||
|
||||
### 3. No Code Changes Required
|
||||
- ✅ No Java/Kotlin code changes needed
|
||||
- ✅ No AndroidManifest.xml changes needed
|
||||
- ✅ No permission handling code changes needed
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
After upgrading to API 23, test:
|
||||
|
||||
- [ ] App builds successfully
|
||||
- [ ] App installs on API 23 device/emulator
|
||||
- [ ] Camera functionality works (permissions requested)
|
||||
- [ ] File access works (permissions requested)
|
||||
- [ ] Share functionality works
|
||||
- [ ] QR code scanning works
|
||||
- [ ] Deep linking works
|
||||
- [ ] All Capacitor plugins work correctly
|
||||
- [ ] No crashes or permission-related errors
|
||||
- [ ] APK size is acceptable
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise:
|
||||
|
||||
1. Revert `android/variables.gradle` to `minSdkVersion = 22`
|
||||
2. Rebuild and test
|
||||
3. Document issues encountered
|
||||
4. Address issues before retrying upgrade
|
||||
|
||||
## Recommendation
|
||||
|
||||
### ✅ **Proceed with Upgrade**
|
||||
|
||||
**Reasons:**
|
||||
1. **Minimal Code Impact:** Only one line needs to change
|
||||
2. **Already Compatible:** App uses Capacitor which handles runtime permissions
|
||||
3. **Device Impact:** Negligible (~0.1% of devices)
|
||||
4. **Capacitor Compliance:** Officially meets Capacitor 6 requirements
|
||||
5. **Future-Proofing:** Better alignment with modern Android development
|
||||
|
||||
**Timeline:**
|
||||
- **Low Risk:** Can be done anytime
|
||||
- **Recommended:** Before implementing SharedImagePlugin (cleaner baseline)
|
||||
- **Testing:** 1-2 hours of testing on API 23+ devices
|
||||
|
||||
## Migration Steps
|
||||
|
||||
1. **Update Build Configuration:**
|
||||
```bash
|
||||
# Edit android/variables.gradle
|
||||
minSdkVersion = 23
|
||||
```
|
||||
|
||||
2. **Sync Gradle:**
|
||||
```bash
|
||||
cd android
|
||||
./gradlew clean
|
||||
```
|
||||
|
||||
3. **Build and Test:**
|
||||
```bash
|
||||
npm run build:android:test
|
||||
# Test on API 23+ device/emulator
|
||||
```
|
||||
|
||||
4. **Verify Permissions:**
|
||||
- Test camera access
|
||||
- Test file access
|
||||
- Verify permission dialogs appear
|
||||
|
||||
5. **Update Documentation:**
|
||||
- Update any docs mentioning API 22
|
||||
- Update implementation plan
|
||||
|
||||
## Summary
|
||||
|
||||
| Aspect | Impact | Status |
|
||||
|--------|--------|--------|
|
||||
| **Code Changes** | None required | ✅ Safe |
|
||||
| **Dependencies** | All compatible | ✅ Safe |
|
||||
| **Permissions** | Already handled | ✅ Safe |
|
||||
| **Device Coverage** | ~0.1% loss | ⚠️ Minimal |
|
||||
| **Build Config** | 1 line change | ✅ Simple |
|
||||
| **Testing** | Standard testing | ✅ Required |
|
||||
| **Risk Level** | Low | ✅ Low Risk |
|
||||
|
||||
**Final Recommendation:** Proceed with upgrade. The benefits (Capacitor compliance, future-proofing) outweigh the minimal risks (negligible device loss, no code changes needed).
|
||||
|
||||
139
doc/ios-share-extension-git-commit-guide.md
Normal file
139
doc/ios-share-extension-git-commit-guide.md
Normal file
@@ -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.
|
||||
|
||||
283
doc/ios-share-extension-improvements.md
Normal file
283
doc/ios-share-extension-improvements.md
Normal file
@@ -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
|
||||
|
||||
140
doc/ios-share-extension-setup.md
Normal file
140
doc/ios-share-extension-setup.md
Normal file
@@ -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)
|
||||
|
||||
93
doc/ios-share-implementation-status.md
Normal file
93
doc/ios-share-implementation-status.md
Normal file
@@ -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
|
||||
|
||||
507
doc/native-share-target-implementation.md
Normal file
507
doc/native-share-target-implementation.md
Normal file
@@ -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
|
||||
<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")
|
||||
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
|
||||
<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)
|
||||
|
||||
528
doc/shared-image-plugin-implementation-plan.md
Normal file
528
doc/shared-image-plugin-implementation-plan.md
Normal file
@@ -0,0 +1,528 @@
|
||||
# Shared Image Plugin Implementation Plan
|
||||
|
||||
**Date:** 2025-12-03 15:40:38 PST
|
||||
**Status:** Planning
|
||||
**Goal:** Replace temp file approach with native Capacitor plugins for iOS and Android
|
||||
|
||||
## Minimum OS Version Compatibility Analysis
|
||||
|
||||
### Current Project Configuration:
|
||||
- **iOS Deployment Target**: 13.0 (Podfile and Xcode project)
|
||||
- **Android minSdkVersion**: 23 (API 23 - Android 6.0 Marshmallow) ✅ **Upgraded**
|
||||
- **Capacitor Version**: 6.2.0
|
||||
|
||||
### Capacitor 6 Requirements:
|
||||
- **iOS**: Requires iOS 13.0+ ✅ **Compatible** (current: 13.0)
|
||||
- **Android**: Requires API 23+ ✅ **Compatible** (current: API 23)
|
||||
|
||||
### Plugin API Compatibility:
|
||||
|
||||
#### iOS Plugin APIs:
|
||||
- ✅ `CAPPlugin` base class: Available in iOS 13.0+ (Capacitor requirement)
|
||||
- ✅ `CAPPluginCall`: Available in iOS 13.0+ (Capacitor requirement)
|
||||
- ✅ `UserDefaults(suiteName:)`: Available since iOS 8.0 (well below iOS 13.0)
|
||||
- ✅ `@objc` annotations: Available since iOS 8.0
|
||||
- ✅ Swift 5.0: Compatible with iOS 13.0+
|
||||
|
||||
**Conclusion**: iOS 13.0 is fully compatible with the plugin implementation. **No iOS version update required.**
|
||||
|
||||
#### Android Plugin APIs:
|
||||
- ✅ `Plugin` base class: Available in API 21+ (Capacitor requirement)
|
||||
- ✅ `PluginCall`: Available in API 21+ (Capacitor requirement)
|
||||
- ✅ `SharedPreferences`: Available since API 1 (works on all Android versions)
|
||||
- ✅ `@CapacitorPlugin` annotation: Available in API 21+ (Capacitor requirement)
|
||||
- ✅ `@PluginMethod` annotation: Available in API 21+ (Capacitor requirement)
|
||||
|
||||
**Conclusion**: Android API 23 is fully compatible with the plugin implementation and officially meets Capacitor 6 requirements. ✅ **No Android version concerns.**
|
||||
|
||||
### Share Extension Compatibility:
|
||||
- **iOS Share Extension**: Uses same deployment target as main app (iOS 13.0)
|
||||
- **App Group**: Available since iOS 8.0, fully compatible
|
||||
- No additional version requirements for share extension functionality
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the migration from the current temp file approach to implementing dedicated Capacitor plugins for handling shared images. This will eliminate file I/O, polling, and timing issues, providing a more direct and reliable native-to-JS bridge.
|
||||
|
||||
## Current Implementation Issues
|
||||
|
||||
### Temp File Approach Problems:
|
||||
1. **Timing Issues**: Requires polling with exponential backoff to wait for file creation
|
||||
2. **Race Conditions**: File may not exist when JS checks, or may be read multiple times
|
||||
3. **File Management**: Need to delete temp files after reading to prevent re-processing
|
||||
4. **Platform Differences**: Different directories (Documents vs Data) add complexity
|
||||
5. **Error Handling**: File I/O errors can be hard to debug
|
||||
6. **Performance**: File system operations are slower than direct native calls
|
||||
|
||||
## Proposed Solution: Capacitor Plugins
|
||||
|
||||
### Benefits:
|
||||
- ✅ Direct native-to-JS communication (no file I/O)
|
||||
- ✅ Synchronous/async method calls (no polling needed)
|
||||
- ✅ Type-safe TypeScript interfaces
|
||||
- ✅ Better error handling and debugging
|
||||
- ✅ Lower latency
|
||||
- ✅ More maintainable and follows Capacitor best practices
|
||||
|
||||
## Implementation Layout
|
||||
|
||||
### 1. iOS Plugin Implementation
|
||||
|
||||
#### 1.1 Create iOS Plugin File
|
||||
**Location:** `ios/App/App/SharedImagePlugin.swift`
|
||||
|
||||
**Structure:**
|
||||
```swift
|
||||
import Foundation
|
||||
import Capacitor
|
||||
|
||||
@objc(SharedImagePlugin)
|
||||
public class SharedImagePlugin: CAPPlugin {
|
||||
private let appGroupIdentifier = "group.app.timesafari"
|
||||
|
||||
@objc func getSharedImage(_ call: CAPPluginCall) {
|
||||
// Read from App Group UserDefaults
|
||||
// Return base64 and fileName
|
||||
// Clear data after reading
|
||||
}
|
||||
|
||||
@objc func hasSharedImage(_ call: CAPPluginCall) {
|
||||
// Check if shared image exists without reading it
|
||||
// Useful for quick checks
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Use existing `getSharedImageData()` logic from AppDelegate
|
||||
- Return data as JSObject with `base64` and `fileName` keys
|
||||
- Clear UserDefaults after reading to prevent re-reading
|
||||
- Handle errors gracefully with `call.reject()`
|
||||
- **Version Compatibility**: Works with iOS 13.0+ (current deployment target)
|
||||
|
||||
#### 1.2 Register Plugin in iOS
|
||||
**Location:** `ios/App/App/AppDelegate.swift`
|
||||
|
||||
**Changes:**
|
||||
- Remove `writeSharedImageToTempFile()` method
|
||||
- Remove temp file writing from `application(_:open:options:)`
|
||||
- Remove temp file writing from `checkForSharedImageOnActivation()`
|
||||
- Keep `getSharedImageData()` method (or move to plugin)
|
||||
- Plugin auto-registers via Capacitor's plugin system
|
||||
|
||||
**Note:** Capacitor plugins are auto-discovered if they follow naming conventions and are in the app bundle.
|
||||
|
||||
### 2. Android Plugin Implementation
|
||||
|
||||
#### 2.1 Create Android Plugin File
|
||||
**Location:** `android/app/src/main/java/app/timesafari/sharedimage/SharedImagePlugin.java`
|
||||
|
||||
**Structure:**
|
||||
```java
|
||||
package app.timesafari.sharedimage;
|
||||
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
@CapacitorPlugin(name = "SharedImage")
|
||||
public class SharedImagePlugin extends Plugin {
|
||||
|
||||
@PluginMethod
|
||||
public void getSharedImage(PluginCall call) {
|
||||
// Read from SharedPreferences or Intent extras
|
||||
// Return base64 and fileName
|
||||
// Clear data after reading
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void hasSharedImage(PluginCall call) {
|
||||
// Check if shared image exists without reading it
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Use SharedPreferences to store shared image data between share intent and plugin call
|
||||
- Store base64 and fileName when processing share intent
|
||||
- Read and clear in `getSharedImage()` method
|
||||
- Handle Intent extras if app was just launched
|
||||
- **Version Compatibility**: Works with Android API 22+ (current minSdkVersion)
|
||||
|
||||
#### 2.2 Update MainActivity
|
||||
**Location:** `android/app/src/main/java/app/timesafari/MainActivity.java`
|
||||
|
||||
**Changes:**
|
||||
- Remove `writeSharedImageToTempFile()` method
|
||||
- Remove `TEMP_FILE_NAME` constant
|
||||
- Update `processSharedImage()` to store in SharedPreferences instead of file
|
||||
- Register plugin: `registerPlugin(SharedImagePlugin.class);`
|
||||
- Store shared image data in SharedPreferences when processing share intent
|
||||
|
||||
**SharedPreferences Approach:**
|
||||
```java
|
||||
// In processSharedImage():
|
||||
SharedPreferences prefs = getSharedPreferences("shared_image", MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putString("base64", base64String);
|
||||
editor.putString("fileName", actualFileName);
|
||||
editor.putBoolean("hasSharedImage", true);
|
||||
editor.apply();
|
||||
```
|
||||
|
||||
### 3. TypeScript/JavaScript Integration
|
||||
|
||||
#### 3.1 Create TypeScript Plugin Definition
|
||||
**Location:** `src/plugins/SharedImagePlugin.ts` (new file)
|
||||
|
||||
**Structure:**
|
||||
```typescript
|
||||
import { registerPlugin } from '@capacitor/core';
|
||||
|
||||
export interface SharedImageResult {
|
||||
base64: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface SharedImagePlugin {
|
||||
getSharedImage(): Promise<SharedImageResult | null>;
|
||||
hasSharedImage(): Promise<{ hasImage: boolean }>;
|
||||
}
|
||||
|
||||
const SharedImage = registerPlugin<SharedImagePlugin>('SharedImage', {
|
||||
web: () => import('./SharedImagePlugin.web').then(m => new m.SharedImagePluginWeb()),
|
||||
});
|
||||
|
||||
export * from './definitions';
|
||||
export { SharedImage };
|
||||
```
|
||||
|
||||
#### 3.2 Create Web Implementation (for development)
|
||||
**Location:** `src/plugins/SharedImagePlugin.web.ts` (new file)
|
||||
|
||||
**Structure:**
|
||||
```typescript
|
||||
import { WebPlugin } from '@capacitor/core';
|
||||
import type { SharedImagePlugin, SharedImageResult } from './definitions';
|
||||
|
||||
export class SharedImagePluginWeb extends WebPlugin implements SharedImagePlugin {
|
||||
async getSharedImage(): Promise<SharedImageResult | null> {
|
||||
// Return null for web platform
|
||||
return null;
|
||||
}
|
||||
|
||||
async hasSharedImage(): Promise<{ hasImage: boolean }> {
|
||||
return { hasImage: false };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 Create Type Definitions
|
||||
**Location:** `src/plugins/definitions.ts` (new file)
|
||||
|
||||
**Structure:**
|
||||
```typescript
|
||||
export interface SharedImageResult {
|
||||
base64: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface SharedImagePlugin {
|
||||
getSharedImage(): Promise<SharedImageResult | null>;
|
||||
hasSharedImage(): Promise<{ hasImage: boolean }>;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.4 Update main.capacitor.ts
|
||||
**Location:** `src/main.capacitor.ts`
|
||||
|
||||
**Changes:**
|
||||
- Remove `pollForFileExistence()` function
|
||||
- Remove temp file reading logic from `checkAndStoreNativeSharedImage()`
|
||||
- Replace with direct plugin call:
|
||||
|
||||
```typescript
|
||||
async function checkAndStoreNativeSharedImage(): Promise<{
|
||||
success: boolean;
|
||||
fileName?: string;
|
||||
}> {
|
||||
if (isProcessingSharedImage) {
|
||||
logger.debug("[Main] ⏸️ Shared image processing already in progress, skipping");
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
isProcessingSharedImage = true;
|
||||
|
||||
try {
|
||||
if (!Capacitor.isNativePlatform() ||
|
||||
(Capacitor.getPlatform() !== "ios" && Capacitor.getPlatform() !== "android")) {
|
||||
isProcessingSharedImage = false;
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// Direct plugin call - no polling needed!
|
||||
const { SharedImage } = await import('./plugins/SharedImagePlugin');
|
||||
const result = await SharedImage.getSharedImage();
|
||||
|
||||
if (result && result.base64) {
|
||||
await storeSharedImageInTempDB(result.base64, result.fileName);
|
||||
isProcessingSharedImage = false;
|
||||
return { success: true, fileName: result.fileName };
|
||||
}
|
||||
|
||||
isProcessingSharedImage = false;
|
||||
return { success: false };
|
||||
} catch (error) {
|
||||
logger.error("[Main] Error checking for native shared image:", error);
|
||||
isProcessingSharedImage = false;
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Remove:**
|
||||
- `pollForFileExistence()` function (lines 71-98)
|
||||
- All Filesystem plugin imports related to temp file reading
|
||||
- Temp file path constants and directory logic
|
||||
|
||||
### 4. Data Flow Comparison
|
||||
|
||||
#### Current (Temp File) Flow:
|
||||
```
|
||||
Share Extension/Intent
|
||||
↓
|
||||
Native writes temp file
|
||||
↓
|
||||
JS polls for file existence (with retries)
|
||||
↓
|
||||
JS reads file via Filesystem plugin
|
||||
↓
|
||||
JS parses JSON
|
||||
↓
|
||||
JS deletes temp file
|
||||
↓
|
||||
JS stores in temp DB
|
||||
```
|
||||
|
||||
#### New (Plugin) Flow:
|
||||
```
|
||||
Share Extension/Intent
|
||||
↓
|
||||
Native stores in UserDefaults/SharedPreferences
|
||||
↓
|
||||
JS calls plugin.getSharedImage()
|
||||
↓
|
||||
Native reads and clears data
|
||||
↓
|
||||
Native returns data directly
|
||||
↓
|
||||
JS stores in temp DB
|
||||
```
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
### New Files to Create:
|
||||
1. `ios/App/App/SharedImagePlugin.swift` - iOS plugin implementation
|
||||
2. `android/app/src/main/java/app/timesafari/sharedimage/SharedImagePlugin.java` - Android plugin
|
||||
3. `src/plugins/SharedImagePlugin.ts` - TypeScript plugin registration
|
||||
4. `src/plugins/SharedImagePlugin.web.ts` - Web fallback implementation
|
||||
5. `src/plugins/definitions.ts` - TypeScript type definitions
|
||||
|
||||
### Files to Modify:
|
||||
1. `ios/App/App/AppDelegate.swift` - Remove temp file writing
|
||||
2. `android/app/src/main/java/app/timesafari/MainActivity.java` - Remove temp file writing, add SharedPreferences
|
||||
3. `src/main.capacitor.ts` - Replace temp file logic with plugin calls
|
||||
|
||||
### Files to Remove:
|
||||
- No files need to be deleted, but code will be removed from existing files
|
||||
|
||||
## Implementation Considerations
|
||||
|
||||
### 1. Data Storage Strategy
|
||||
|
||||
#### iOS:
|
||||
- **Current**: App Group UserDefaults (already working)
|
||||
- **Plugin**: Read from same UserDefaults, no changes needed
|
||||
- **Clearing**: Clear immediately after reading in plugin method
|
||||
|
||||
#### Android:
|
||||
- **Current**: Temp file in app's internal files directory
|
||||
- **New**: SharedPreferences (persistent key-value store)
|
||||
- **Alternative**: Could use Intent extras if app is launched fresh, but SharedPreferences is more reliable for backgrounded apps
|
||||
|
||||
### 2. Timing and Lifecycle
|
||||
|
||||
#### When to Check for Shared Images:
|
||||
1. **App Launch**: Check in `checkForSharedImageAndNavigate()` (already exists)
|
||||
2. **App Becomes Active**: Check in `appStateChange` listener (already exists)
|
||||
3. **Deep Link**: Check in `handleDeepLink()` for empty path URLs (already exists)
|
||||
|
||||
#### Plugin Call Timing:
|
||||
- Plugin calls are synchronous from JS perspective
|
||||
- No polling needed - native side handles data availability
|
||||
- If no data exists, plugin returns `null` immediately
|
||||
|
||||
### 3. Error Handling
|
||||
|
||||
#### Plugin Error Scenarios:
|
||||
- **No shared image**: Return `null` (not an error)
|
||||
- **Data corruption**: Return error via `call.reject()`
|
||||
- **Missing permissions**: Return error (shouldn't happen with App Group/SharedPreferences)
|
||||
|
||||
#### JS Error Handling:
|
||||
- Wrap plugin calls in try-catch
|
||||
- Log errors appropriately
|
||||
- Don't crash app if plugin fails
|
||||
|
||||
### 4. Backward Compatibility
|
||||
|
||||
#### Migration Path:
|
||||
- Keep temp file code temporarily (commented out) for rollback
|
||||
- Test thoroughly on both platforms
|
||||
- Remove temp file code after verification
|
||||
|
||||
### 5. Testing Considerations
|
||||
|
||||
#### Test Cases:
|
||||
1. **Share from Photos app** → Verify image appears in app
|
||||
2. **Share while app is backgrounded** → Verify image appears when app becomes active
|
||||
3. **Share while app is closed** → Verify image appears on app launch
|
||||
4. **Multiple rapid shares** → Verify only latest image is processed
|
||||
5. **Share then close app before processing** → Verify image persists
|
||||
6. **Share then clear app data** → Verify graceful handling
|
||||
|
||||
#### Edge Cases:
|
||||
- Very large images (memory concerns)
|
||||
- Multiple images shared simultaneously
|
||||
- App killed by OS before processing
|
||||
- Network interruptions during processing
|
||||
|
||||
### 6. Performance Considerations
|
||||
|
||||
#### Benefits:
|
||||
- **Latency**: Direct calls vs file I/O (faster)
|
||||
- **CPU**: No polling overhead
|
||||
- **Memory**: No temp file storage
|
||||
- **Battery**: Less file system activity
|
||||
|
||||
#### Potential Issues:
|
||||
- Large base64 strings in memory (same as current approach)
|
||||
- UserDefaults/SharedPreferences size limits (shouldn't be an issue for single image)
|
||||
|
||||
### 7. Type Safety
|
||||
|
||||
#### TypeScript Benefits:
|
||||
- Full type checking for plugin methods
|
||||
- Autocomplete in IDE
|
||||
- Compile-time error checking
|
||||
- Better developer experience
|
||||
|
||||
### 8. Plugin Registration
|
||||
|
||||
#### iOS:
|
||||
- Capacitor auto-discovers plugins via naming convention
|
||||
- Ensure plugin is in app target (not extension target)
|
||||
- No manual registration needed in AppDelegate
|
||||
|
||||
#### Android:
|
||||
- Register in `MainActivity.onCreate()`:
|
||||
```java
|
||||
registerPlugin(SharedImagePlugin.class);
|
||||
```
|
||||
|
||||
### 9. Capacitor Version Compatibility
|
||||
|
||||
#### Check Current Version:
|
||||
- Verify Capacitor version supports custom plugins
|
||||
- Ensure plugin API hasn't changed
|
||||
- Test with current Capacitor version first
|
||||
|
||||
### 10. Build and Deployment
|
||||
|
||||
#### Build Steps:
|
||||
1. Create plugin files
|
||||
2. Register Android plugin in MainActivity
|
||||
3. Update TypeScript code
|
||||
4. Test on iOS simulator
|
||||
5. Test on Android emulator
|
||||
6. Test on physical devices
|
||||
7. Remove temp file code
|
||||
8. Update documentation
|
||||
|
||||
#### Deployment:
|
||||
- No changes to build scripts needed
|
||||
- No changes to CI/CD needed
|
||||
- No changes to app configuration needed
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Phase 1: Create Plugins (Non-Breaking)
|
||||
1. Create iOS plugin file
|
||||
2. Create Android plugin file
|
||||
3. Create TypeScript definitions
|
||||
4. Register Android plugin
|
||||
5. Test plugins independently (don't use in main code yet)
|
||||
|
||||
### Phase 2: Update JS Integration (Breaking)
|
||||
1. Create TypeScript plugin wrapper
|
||||
2. Update `checkAndStoreNativeSharedImage()` to use plugin
|
||||
3. Remove temp file reading logic
|
||||
4. Test on both platforms
|
||||
|
||||
### Phase 3: Cleanup Native Code (Breaking)
|
||||
1. Remove temp file writing from iOS AppDelegate
|
||||
2. Remove temp file writing from Android MainActivity
|
||||
3. Update to use SharedPreferences on Android
|
||||
4. Test thoroughly
|
||||
|
||||
### Phase 4: Final Cleanup
|
||||
1. Remove `pollForFileExistence()` function
|
||||
2. Remove Filesystem imports related to temp files
|
||||
3. Update comments and documentation
|
||||
4. Final testing
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If issues arise:
|
||||
1. Revert JS changes to use temp file approach
|
||||
2. Re-enable temp file writing in native code
|
||||
3. Keep plugins for future migration attempt
|
||||
4. Document issues encountered
|
||||
|
||||
## Success Criteria
|
||||
|
||||
✅ Plugin methods work on both iOS and Android
|
||||
✅ No polling or file I/O needed
|
||||
✅ Shared images appear correctly in app
|
||||
✅ No memory leaks or performance issues
|
||||
✅ Error handling works correctly
|
||||
✅ All test cases pass
|
||||
✅ Code is cleaner and more maintainable
|
||||
|
||||
## Additional Notes
|
||||
|
||||
### iOS App Group:
|
||||
- Current App Group ID: `group.app.timesafari`
|
||||
- Ensure plugin has access to same App Group
|
||||
- Share Extension already writes to this App Group
|
||||
|
||||
### Android Share Intent:
|
||||
- Current implementation handles `ACTION_SEND` and `ACTION_SEND_MULTIPLE`
|
||||
- SharedPreferences key: `shared_image` (or similar)
|
||||
- Store both base64 and fileName
|
||||
|
||||
### Future Enhancements:
|
||||
- Consider adding event listeners for real-time notifications
|
||||
- Could add method to clear shared image without reading
|
||||
- Could add method to get image metadata without full data
|
||||
|
||||
## References
|
||||
|
||||
- [Capacitor Plugin Development Guide](https://capacitorjs.com/docs/plugins)
|
||||
- Existing plugin example: `SafeAreaPlugin.java`
|
||||
- Current temp file implementation: `main.capacitor.ts` lines 166-271
|
||||
- iOS AppDelegate: `ios/App/App/AppDelegate.swift`
|
||||
- Android MainActivity: `android/app/src/main/java/app/timesafari/MainActivity.java`
|
||||
|
||||
329
doc/shared-image-plugin-pre-implementation-decisions.md
Normal file
329
doc/shared-image-plugin-pre-implementation-decisions.md
Normal file
@@ -0,0 +1,329 @@
|
||||
# Shared Image Plugin - Pre-Implementation Decision Checklist
|
||||
|
||||
**Date:** 2025-12-03
|
||||
**Status:** Pre-Implementation Planning
|
||||
**Purpose:** Identify and document decisions needed before implementing SharedImagePlugin
|
||||
|
||||
## ✅ Completed Decisions
|
||||
|
||||
### 1. Minimum OS Versions
|
||||
- ✅ **iOS**: Keep at 13.0 (no changes needed)
|
||||
- ✅ **Android**: Upgraded from API 22 to API 23 (completed)
|
||||
- ✅ **Rationale**: Meets Capacitor 6 requirements, minimal device impact
|
||||
|
||||
### 2. Data Storage Strategy
|
||||
- ✅ **iOS**: Use App Group UserDefaults (already implemented in Share Extension)
|
||||
- ✅ **Android**: Use SharedPreferences (to be implemented)
|
||||
- ✅ **Rationale**: Direct, efficient, no file I/O needed
|
||||
|
||||
## 🔍 Decisions Needed Before Implementation
|
||||
|
||||
### 1. Plugin Method Design
|
||||
|
||||
#### Decision: What methods should the plugin expose?
|
||||
|
||||
**Options:**
|
||||
- **Option A (Minimal)**: Only `getSharedImage()` - read and clear in one call
|
||||
- **Option B (Recommended)**: `getSharedImage()` + `hasSharedImage()` - allows checking without reading
|
||||
- **Option C (Extended)**: Add `clearSharedImage()` - explicit clearing without reading
|
||||
|
||||
**Recommendation:** **Option B**
|
||||
- `getSharedImage()`: Returns `{ base64: string, fileName: string } | null`
|
||||
- `hasSharedImage()`: Returns `{ hasImage: boolean }` - useful for quick checks
|
||||
|
||||
**Decision Needed:** ✅ Confirm Option B or choose alternative
|
||||
|
||||
---
|
||||
|
||||
### 2. Error Handling Strategy
|
||||
|
||||
#### Decision: How should the plugin handle errors?
|
||||
|
||||
**Options:**
|
||||
- **Option A**: Return `null` for all errors (no shared image = no error)
|
||||
- **Option B**: Use `call.reject()` for actual errors, return `null` only when no image exists
|
||||
- **Option C**: Return error object in result: `{ error: string } | { base64: string, fileName: string }`
|
||||
|
||||
**Recommendation:** **Option B**
|
||||
- `getSharedImage()` returns `null` when no image exists (normal case)
|
||||
- `call.reject()` for actual errors (UserDefaults unavailable, data corruption, etc.)
|
||||
- Clear distinction between "no data" (normal) vs "error" (exceptional)
|
||||
|
||||
**Decision Needed:** ✅ Confirm Option B or choose alternative
|
||||
|
||||
---
|
||||
|
||||
### 3. Data Clearing Strategy
|
||||
|
||||
#### Decision: When should shared image data be cleared?
|
||||
|
||||
**Current Behavior (temp file approach):**
|
||||
- Data cleared after reading (immediate)
|
||||
|
||||
**Options:**
|
||||
- **Option A**: Clear immediately after reading (current behavior)
|
||||
- **Option B**: Clear on next read (allow re-reading until consumed)
|
||||
- **Option C**: Clear after successful storage in temp DB (JS confirms receipt)
|
||||
|
||||
**Recommendation:** **Option A** (immediate clearing)
|
||||
- Prevents accidental re-reading
|
||||
- Simpler implementation
|
||||
- Matches current behavior
|
||||
- If JS fails to store, user can share again
|
||||
|
||||
**Decision Needed:** ✅ Confirm Option A or choose alternative
|
||||
|
||||
---
|
||||
|
||||
### 4. iOS Plugin Registration
|
||||
|
||||
#### Decision: How should the iOS plugin be registered?
|
||||
|
||||
**Options:**
|
||||
- **Option A**: Auto-discovery (Capacitor finds plugins by naming convention)
|
||||
- **Option B**: Manual registration in AppDelegate
|
||||
- **Option C**: Hybrid (auto-discovery with manual registration as fallback)
|
||||
|
||||
**Recommendation:** **Option A** (auto-discovery)
|
||||
- Follows Capacitor best practices
|
||||
- Less code to maintain
|
||||
- Other plugins in project use auto-discovery (SafeAreaPlugin uses manual, but that's older pattern)
|
||||
|
||||
**Note:** Need to verify plugin naming convention:
|
||||
- Class name: `SharedImagePlugin`
|
||||
- File name: `SharedImagePlugin.swift`
|
||||
- Location: `ios/App/App/SharedImagePlugin.swift`
|
||||
|
||||
**Decision Needed:** ✅ Confirm Option A, or if auto-discovery doesn't work, use Option B
|
||||
|
||||
---
|
||||
|
||||
### 5. TypeScript Interface Design
|
||||
|
||||
#### Decision: What should the TypeScript interface look like?
|
||||
|
||||
**Proposed Interface:**
|
||||
```typescript
|
||||
export interface SharedImageResult {
|
||||
base64: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface SharedImagePlugin {
|
||||
getSharedImage(): Promise<SharedImageResult | null>;
|
||||
hasSharedImage(): Promise<{ hasImage: boolean }>;
|
||||
}
|
||||
```
|
||||
|
||||
**Questions:**
|
||||
- Should `fileName` be optional? (Currently always provided, but could be empty string)
|
||||
- Should we include metadata (image size, MIME type)?
|
||||
- Should `hasSharedImage()` return more info (like fileName without reading)?
|
||||
|
||||
**Recommendation:** Keep simple for now:
|
||||
- `fileName` is always a string (may be default "shared-image.jpg")
|
||||
- No metadata initially (can add later if needed)
|
||||
- `hasSharedImage()` only returns boolean (keep it lightweight)
|
||||
|
||||
**Decision Needed:** ✅ Confirm interface design or request changes
|
||||
|
||||
---
|
||||
|
||||
### 6. Android Data Storage Timing
|
||||
|
||||
#### Decision: When should Android store shared image data in SharedPreferences?
|
||||
|
||||
**Current Flow:**
|
||||
1. Share intent received in MainActivity
|
||||
2. Image processed and written to temp file
|
||||
3. JS reads temp file
|
||||
|
||||
**New Flow Options:**
|
||||
- **Option A**: Store in SharedPreferences immediately when share intent received (in `processSharedImage()`)
|
||||
- **Option B**: Store when plugin is first called (lazy loading)
|
||||
- **Option C**: Store in both places during transition (backward compatibility)
|
||||
|
||||
**Recommendation:** **Option A** (immediate storage)
|
||||
- Data available immediately when plugin is called
|
||||
- No timing issues
|
||||
- Matches iOS pattern (data stored by Share Extension)
|
||||
|
||||
**Implementation:**
|
||||
- Update `processSharedImage()` in MainActivity to store in SharedPreferences
|
||||
- Remove temp file writing
|
||||
- Plugin reads from SharedPreferences
|
||||
|
||||
**Decision Needed:** ✅ Confirm Option A
|
||||
|
||||
---
|
||||
|
||||
### 7. Migration Strategy
|
||||
|
||||
#### Decision: How to handle the transition from temp file to plugin?
|
||||
|
||||
**Options:**
|
||||
- **Option A (Clean Break)**: Remove temp file code immediately, use plugin only
|
||||
- **Option B (Gradual)**: Support both approaches temporarily, remove temp file later
|
||||
- **Option C (Feature Flag)**: Use feature flag to switch between approaches
|
||||
|
||||
**Recommendation:** **Option A** (clean break)
|
||||
- Simpler implementation
|
||||
- Less code to maintain
|
||||
- Temp file approach is buggy anyway (why we're replacing it)
|
||||
- Can rollback via git if needed
|
||||
|
||||
**Rollback Plan:**
|
||||
- Keep temp file code in git history
|
||||
- If plugin has issues, can revert commit
|
||||
- Test thoroughly before removing temp file code
|
||||
|
||||
**Decision Needed:** ✅ Confirm Option A
|
||||
|
||||
---
|
||||
|
||||
### 8. Plugin Naming
|
||||
|
||||
#### Decision: What should the plugin be named?
|
||||
|
||||
**Options:**
|
||||
- **Option A**: `SharedImage` (matches file/class names)
|
||||
- **Option B**: `SharedImagePlugin` (more explicit)
|
||||
- **Option C**: `NativeShare` (more generic, could handle other share types)
|
||||
|
||||
**Recommendation:** **Option A** (`SharedImage`)
|
||||
- Matches Capacitor naming conventions (plugins are referenced without "Plugin" suffix)
|
||||
- Examples: `Capacitor.Plugins.Camera`, `Capacitor.Plugins.Filesystem`
|
||||
- TypeScript: `SharedImage.getSharedImage()`
|
||||
|
||||
**Decision Needed:** ✅ Confirm Option A
|
||||
|
||||
---
|
||||
|
||||
### 9. iOS: Reuse getSharedImageData() or Move to Plugin?
|
||||
|
||||
#### Decision: Should the plugin reuse AppDelegate's `getSharedImageData()` or implement its own?
|
||||
|
||||
**Current Code:**
|
||||
- `AppDelegate.getSharedImageData()` exists and works
|
||||
- Reads from App Group UserDefaults
|
||||
- Clears data after reading
|
||||
|
||||
**Options:**
|
||||
- **Option A**: Plugin calls `getSharedImageData()` from AppDelegate
|
||||
- **Option B**: Plugin implements its own logic (duplicate code)
|
||||
- **Option C**: Move `getSharedImageData()` to a shared utility, both use it
|
||||
|
||||
**Recommendation:** **Option C** (shared utility)
|
||||
- DRY principle
|
||||
- Single source of truth
|
||||
- But: May be overkill for simple logic
|
||||
|
||||
**Alternative Recommendation:** **Option B** (plugin implements own logic)
|
||||
- Plugin is self-contained
|
||||
- No dependency on AppDelegate
|
||||
- Logic is simple (just UserDefaults read/clear)
|
||||
- Can remove `getSharedImageData()` from AppDelegate after migration
|
||||
|
||||
**Decision:** ✅ **Option C** (shared utility) - **CONFIRMED**
|
||||
- Create shared utility for reading from App Group UserDefaults
|
||||
- Both AppDelegate and plugin use the shared utility
|
||||
- Single source of truth for shared image data access
|
||||
|
||||
---
|
||||
|
||||
### 10. Android: SharedPreferences Key Names
|
||||
|
||||
#### Decision: What keys should be used in SharedPreferences?
|
||||
|
||||
**Proposed Keys:**
|
||||
- `shared_image_base64` - Base64 string
|
||||
- `shared_image_file_name` - File name
|
||||
- `shared_image_ready` - Boolean flag (optional, for quick checks)
|
||||
|
||||
**Alternative:**
|
||||
- Use a single JSON object: `shared_image_data` = `{ base64: "...", fileName: "..." }`
|
||||
|
||||
**Recommendation:** Separate keys (first option)
|
||||
- Simpler to read/write
|
||||
- No JSON parsing needed
|
||||
- Matches iOS pattern (separate UserDefaults keys)
|
||||
- Flag is optional but useful for `hasSharedImage()`
|
||||
|
||||
**Decision Needed:** ✅ Confirm key naming or request changes
|
||||
|
||||
---
|
||||
|
||||
### 11. Testing Strategy
|
||||
|
||||
#### Decision: What testing approach should we use?
|
||||
|
||||
**Options:**
|
||||
- **Option A**: Manual testing only
|
||||
- **Option B**: Manual + automated unit tests for plugin methods
|
||||
- **Option C**: Manual + integration tests
|
||||
|
||||
**Recommendation:** **Option A** (manual testing) for now
|
||||
- Plugins are hard to unit test (require native environment)
|
||||
- Manual testing is sufficient for initial implementation
|
||||
- Can add automated tests later if needed
|
||||
|
||||
**Test Scenarios:**
|
||||
1. Share image from Photos app → Verify appears in app
|
||||
2. Share while app backgrounded → Verify appears when app becomes active
|
||||
3. Share while app closed → Verify appears on app launch
|
||||
4. Multiple rapid shares → Verify only latest is processed
|
||||
5. Share then close app before processing → Verify data persists
|
||||
6. Share then clear app data → Verify graceful handling
|
||||
|
||||
**Decision Needed:** ✅ Confirm testing approach
|
||||
|
||||
---
|
||||
|
||||
### 12. Documentation Updates
|
||||
|
||||
#### Decision: What documentation needs updating?
|
||||
|
||||
**Files to Update:**
|
||||
- ✅ Implementation plan (this document)
|
||||
- ⚠️ `doc/native-share-target-implementation.md` - Update to reflect plugin approach
|
||||
- ⚠️ `doc/ios-share-implementation-status.md` - Mark plugin as implemented
|
||||
- ⚠️ Code comments in `main.capacitor.ts` - Update to reflect plugin usage
|
||||
|
||||
**Decision Needed:** ✅ Confirm documentation update list
|
||||
|
||||
---
|
||||
|
||||
## Summary of Decisions Needed
|
||||
|
||||
| # | Decision | Recommendation | Status |
|
||||
|---|----------|----------------|--------|
|
||||
| 1 | Plugin Methods | Option B: `getSharedImage()` + `hasSharedImage()` | ✅ Confirmed |
|
||||
| 2 | Error Handling | Option B: `null` for no data, `reject()` for errors | ✅ Confirmed |
|
||||
| 3 | Data Clearing | Option A: Clear immediately after reading | ✅ Confirmed |
|
||||
| 4 | iOS Registration | Option A: Auto-discovery | ✅ Confirmed |
|
||||
| 5 | TypeScript Interface | Proposed interface (see above) | ✅ Confirmed |
|
||||
| 6 | Android Storage Timing | Option A: Store immediately on share intent | ✅ Confirmed |
|
||||
| 7 | Migration Strategy | Option A: Clean break, remove temp file code | ✅ Confirmed |
|
||||
| 8 | Plugin Naming | Option A: `SharedImage` | ✅ Confirmed |
|
||||
| 9 | iOS Code Reuse | Option C: Shared utility | ✅ Confirmed |
|
||||
| 10 | Android Key Names | Separate keys: `shared_image_base64`, `shared_image_file_name` | ✅ Confirmed |
|
||||
| 11 | Testing Strategy | Option A: Manual testing | ✅ Confirmed |
|
||||
| 12 | Documentation | Update listed files | ✅ Confirmed |
|
||||
| - | Multiple Images | Single image only (SharedPhotoView requirement) | ✅ Confirmed |
|
||||
| - | Backward Compatibility | No temp file backward compatibility | ✅ Confirmed |
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review this checklist** and confirm or modify recommendations
|
||||
2. **Make decisions** on all pending items
|
||||
3. **Update implementation plan** with confirmed decisions
|
||||
4. **Begin implementation** with clear specifications
|
||||
|
||||
## Questions to Consider
|
||||
|
||||
- Are there any edge cases not covered?
|
||||
- Should we support multiple images (currently only first image)?
|
||||
- Should we add image metadata (size, MIME type) in the future?
|
||||
- Do we need backward compatibility with temp file approach?
|
||||
- Should plugin methods be synchronous or async? (Capacitor plugins are async by default)
|
||||
|
||||
76
doc/xcode-26-cocoapods-workaround.md
Normal file
76
doc/xcode-26-cocoapods-workaround.md
Normal file
@@ -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.
|
||||
|
||||
@@ -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, ); }; };
|
||||
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */; };
|
||||
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8C56E152EE064CA00737D0E /* SharedImagePlugin.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 = "<group>"; };
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||
@@ -28,10 +55,28 @@
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImageUtility.swift; sourceTree = "<group>"; };
|
||||
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedImagePlugin.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
/* 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 = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
@@ -73,6 +127,9 @@
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */,
|
||||
C8C56E132EE0474B00737D0E /* SharedImageUtility.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 = (
|
||||
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */,
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
C8C56E142EE0474B00737D0E /* SharedImageUtility.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 = TimeSafari;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
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 = TimeSafari;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
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 */;
|
||||
|
||||
77
ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme
Normal file
77
ios/App/App.xcodeproj/xcshareddata/xcschemes/App.xcscheme
Normal file
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1630"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "App.app"
|
||||
BlueprintName = "App"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "App.app"
|
||||
BlueprintName = "App"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "App.app"
|
||||
BlueprintName = "App"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
10
ios/App/App/App.entitlements
Normal file
10
ios/App/App/App.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.timesafari</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -12,9 +12,49 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
//let sqlite = SQLite()
|
||||
//sqlite.initialize()
|
||||
|
||||
// Register SharedImage plugin manually after bridge is ready
|
||||
// Try multiple times with increasing delays to ensure bridge is initialized
|
||||
var attempts = 0
|
||||
let maxAttempts = 5
|
||||
|
||||
func tryRegister() {
|
||||
attempts += 1
|
||||
if registerSharedImagePlugin() {
|
||||
print("[AppDelegate] ✅ Plugin registration successful on attempt \(attempts)")
|
||||
} else if attempts < maxAttempts {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + Double(attempts) * 0.5) {
|
||||
tryRegister()
|
||||
}
|
||||
} else {
|
||||
print("[AppDelegate] ⚠️ Failed to register plugin after \(maxAttempts) attempts")
|
||||
}
|
||||
}
|
||||
|
||||
// Start registration attempts
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
tryRegister()
|
||||
}
|
||||
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func registerSharedImagePlugin() -> Bool {
|
||||
guard let window = self.window,
|
||||
let bridgeVC = window.rootViewController as? CAPBridgeViewController,
|
||||
let bridge = bridgeVC.bridge else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Create plugin instance
|
||||
// The @objc(SharedImage) annotation makes it available as "SharedImage" to Objective-C
|
||||
// which matches the JavaScript registration name
|
||||
let pluginInstance = SharedImagePlugin()
|
||||
bridge.registerPluginInstance(pluginInstance)
|
||||
print("[AppDelegate] ✅ Registered SharedImagePlugin (exposed as 'SharedImage' via @objc annotation)")
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillResignActive(_ application: UIApplication) {
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
@@ -32,6 +72,26 @@ 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
|
||||
* Note: JavaScript will read the shared image via SharedImagePlugin, so we just check the flag
|
||||
*/
|
||||
private func checkForSharedImageOnActivation() {
|
||||
// Check if shared photo is ready
|
||||
if SharedImageUtility.isSharedPhotoReady() {
|
||||
// Clear the flag
|
||||
SharedImageUtility.clearSharedPhotoReadyFlag()
|
||||
|
||||
// Post notification for JavaScript to handle navigation
|
||||
// JavaScript will read the shared image via SharedImagePlugin
|
||||
NotificationCenter.default.post(name: NSNotification.Name("SharedPhotoReady"), object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
@@ -41,6 +101,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// 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
|
||||
// Note: Share Extension opens app with timesafari:// (empty path), which is handled by JavaScript
|
||||
// via the appUrlOpen listener in main.capacitor.ts
|
||||
return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
@@ -50,5 +112,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
// tracking app url opens, make sure to keep this call
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
66
ios/App/App/SharedImagePlugin.swift
Normal file
66
ios/App/App/SharedImagePlugin.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// SharedImagePlugin.swift
|
||||
// App
|
||||
//
|
||||
// Capacitor plugin for accessing shared image data from Share Extension
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Capacitor
|
||||
|
||||
@objc(SharedImage)
|
||||
public class SharedImagePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
|
||||
// MARK: - CAPBridgedPlugin Conformance
|
||||
|
||||
public var identifier: String {
|
||||
return "SharedImage"
|
||||
}
|
||||
|
||||
public var jsName: String {
|
||||
return "SharedImage"
|
||||
}
|
||||
|
||||
public var pluginMethods: [CAPPluginMethod] {
|
||||
return [
|
||||
CAPPluginMethod(#selector(getSharedImage(_:)), returnType: .promise),
|
||||
CAPPluginMethod(#selector(hasSharedImage(_:)), returnType: .promise)
|
||||
]
|
||||
}
|
||||
|
||||
// MARK: - Plugin Methods
|
||||
|
||||
/**
|
||||
* Get shared image data from App Group UserDefaults
|
||||
* Returns base64 string and fileName, or null if no image exists
|
||||
* Clears the data after reading to prevent re-reading
|
||||
*/
|
||||
@objc public func getSharedImage(_ call: CAPPluginCall) {
|
||||
guard let sharedData = SharedImageUtility.getSharedImageData() else {
|
||||
// No shared image exists - return null (not an error)
|
||||
call.resolve([
|
||||
"base64": NSNull(),
|
||||
"fileName": NSNull()
|
||||
])
|
||||
return
|
||||
}
|
||||
|
||||
// Return the shared image data
|
||||
call.resolve([
|
||||
"base64": sharedData["base64"] ?? "",
|
||||
"fileName": sharedData["fileName"] ?? ""
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shared image exists without reading it
|
||||
* Useful for quick checks before calling getSharedImage()
|
||||
*/
|
||||
@objc public func hasSharedImage(_ call: CAPPluginCall) {
|
||||
let hasImage = SharedImageUtility.hasSharedImage()
|
||||
call.resolve([
|
||||
"hasImage": hasImage
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
107
ios/App/App/SharedImageUtility.swift
Normal file
107
ios/App/App/SharedImageUtility.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// SharedImageUtility.swift
|
||||
// App
|
||||
//
|
||||
// Shared utility for accessing shared image data from App Group container
|
||||
// Images are stored as files in the App Group container to avoid UserDefaults size limits
|
||||
// Used by both AppDelegate and SharedImagePlugin
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class SharedImageUtility {
|
||||
private static let appGroupIdentifier = "group.app.timesafari"
|
||||
private static let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
||||
private static let sharedPhotoFilePathKey = "sharedPhotoFilePath"
|
||||
private static let sharedPhotoReadyKey = "sharedPhotoReady"
|
||||
|
||||
/// Get the App Group container URL for accessing shared files
|
||||
private static var appGroupContainerURL: URL? {
|
||||
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shared image data from App Group container file
|
||||
* All images are stored as files for consistency and to avoid UserDefaults size limits
|
||||
* Clears the data after reading to prevent re-reading
|
||||
*
|
||||
* @returns Dictionary with "base64" and "fileName" keys, or nil if no shared image
|
||||
*/
|
||||
static func getSharedImageData() -> [String: String]? {
|
||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get file path and filename from UserDefaults
|
||||
guard let filePath = userDefaults.string(forKey: sharedPhotoFilePathKey),
|
||||
let containerURL = appGroupContainerURL else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let fileName = userDefaults.string(forKey: sharedPhotoFileNameKey) ?? "shared-image.jpg"
|
||||
let fileURL = containerURL.appendingPathComponent(filePath)
|
||||
|
||||
// Read image data from file
|
||||
guard let imageData = try? Data(contentsOf: fileURL) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert file data to base64 for JavaScript consumption
|
||||
let base64String = imageData.base64EncodedString()
|
||||
|
||||
// Clear the shared data after reading
|
||||
userDefaults.removeObject(forKey: sharedPhotoFilePathKey)
|
||||
userDefaults.removeObject(forKey: sharedPhotoFileNameKey)
|
||||
|
||||
// Remove the file
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
|
||||
userDefaults.synchronize()
|
||||
|
||||
return ["base64": base64String, "fileName": fileName]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shared image exists without reading it
|
||||
*
|
||||
* @returns true if shared image file exists, false otherwise
|
||||
*/
|
||||
static func hasSharedImage() -> Bool {
|
||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier),
|
||||
let filePath = userDefaults.string(forKey: sharedPhotoFilePathKey),
|
||||
let containerURL = appGroupContainerURL else {
|
||||
return false
|
||||
}
|
||||
|
||||
let fileURL = containerURL.appendingPathComponent(filePath)
|
||||
return FileManager.default.fileExists(atPath: fileURL.path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shared photo ready flag is set
|
||||
* This flag is set by the Share Extension when image is ready
|
||||
*
|
||||
* @returns true if flag is set, false otherwise
|
||||
*/
|
||||
static func isSharedPhotoReady() -> Bool {
|
||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||
return false
|
||||
}
|
||||
|
||||
return userDefaults.bool(forKey: sharedPhotoReadyKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the shared photo ready flag
|
||||
* Called after processing the shared image
|
||||
*/
|
||||
static func clearSharedPhotoReadyFlag() {
|
||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||
return
|
||||
}
|
||||
|
||||
userDefaults.removeObject(forKey: sharedPhotoReadyKey)
|
||||
userDefaults.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
21
ios/App/TimeSafariShareExtension/Info.plist
Normal file
21
ios/App/TimeSafariShareExtension/Info.plist
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
207
ios/App/TimeSafariShareExtension/ShareViewController.swift
Normal file
207
ios/App/TimeSafariShareExtension/ShareViewController.swift
Normal file
@@ -0,0 +1,207 @@
|
||||
//
|
||||
// ShareViewController.swift
|
||||
// TimeSafariShareExtension
|
||||
//
|
||||
// Created by Aardimus on 11/24/25.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
class ShareViewController: UIViewController {
|
||||
|
||||
private let appGroupIdentifier = "group.app.timesafari"
|
||||
private let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
||||
private let sharedPhotoFilePathKey = "sharedPhotoFilePath"
|
||||
private let sharedImageFileName = "shared-image"
|
||||
|
||||
/// Get the App Group container URL for storing shared files
|
||||
private var appGroupContainerURL: URL? {
|
||||
return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Set a minimal background (transparent or loading indicator)
|
||||
view.backgroundColor = .systemBackground
|
||||
|
||||
// Process image immediately without showing UI
|
||||
processAndOpenApp()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
processSharedImage(from: inputItems) { [weak self] success in
|
||||
guard let self = self, let context = self.extensionContext else {
|
||||
return
|
||||
}
|
||||
|
||||
if success {
|
||||
// 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 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 {
|
||||
guard let attachments = item.attachments else {
|
||||
continue
|
||||
}
|
||||
|
||||
for attachment in attachments {
|
||||
// Skip non-image attachments
|
||||
guard attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to load raw data first to preserve original format
|
||||
// This preserves the original image format without conversion
|
||||
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
|
||||
guard let self = self else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
if error != nil {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle different image data types
|
||||
// loadItem(forTypeIdentifier:) typically returns URL or Data, not UIImage
|
||||
var imageData: Data?
|
||||
var fileName: String = "shared-image"
|
||||
|
||||
if let url = data as? URL {
|
||||
// Most common case: Image provided as file URL - read raw data to preserve format
|
||||
let accessing = url.startAccessingSecurityScopedResource()
|
||||
defer {
|
||||
if accessing {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
|
||||
// Read raw data directly to preserve original format
|
||||
imageData = try? Data(contentsOf: url)
|
||||
fileName = url.lastPathComponent
|
||||
|
||||
// Fallback: if raw data read fails, try UIImage and convert to PNG (lossless)
|
||||
if imageData == nil, let image = UIImage(contentsOfFile: url.path) {
|
||||
imageData = image.pngData()
|
||||
fileName = self.getFileNameWithExtension(url.lastPathComponent, newExtension: "png")
|
||||
}
|
||||
} else if let data = data as? Data {
|
||||
// Less common: Image provided as raw Data - use directly to preserve format
|
||||
imageData = data
|
||||
fileName = attachment.suggestedName ?? "shared-image"
|
||||
}
|
||||
|
||||
guard let finalImageData = imageData else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Store image as file in App Group container
|
||||
if self.storeImageData(finalImageData, fileName: fileName) {
|
||||
completion(true)
|
||||
} else {
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
return // Process only the first image
|
||||
}
|
||||
}
|
||||
|
||||
// No image found
|
||||
completion(false)
|
||||
}
|
||||
|
||||
/// Helper to get filename with a new extension, preserving base name
|
||||
private func getFileNameWithExtension(_ originalName: String, newExtension: String) -> String {
|
||||
if let nameWithoutExt = originalName.components(separatedBy: ".").first, !nameWithoutExt.isEmpty {
|
||||
return "\(nameWithoutExt).\(newExtension)"
|
||||
}
|
||||
return "shared-image.\(newExtension)"
|
||||
}
|
||||
|
||||
/// Store image data as a file in the App Group container
|
||||
/// All images are stored as files regardless of size for consistency and simplicity
|
||||
/// Returns true if successful, false otherwise
|
||||
private func storeImageData(_ imageData: Data, fileName: String) -> Bool {
|
||||
guard let containerURL = appGroupContainerURL else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Create file URL in the container using the actual filename
|
||||
// Extract extension from fileName if present, otherwise use sharedImageFileName
|
||||
let actualFileName = fileName.isEmpty ? sharedImageFileName : fileName
|
||||
let fileURL = containerURL.appendingPathComponent(actualFileName)
|
||||
|
||||
// Remove old file if it exists
|
||||
try? FileManager.default.removeItem(at: fileURL)
|
||||
|
||||
// Write image data to file
|
||||
do {
|
||||
try imageData.write(to: fileURL)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
// Store file path and filename in UserDefaults (small data, safe to store)
|
||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Store relative path and filename
|
||||
userDefaults.set(actualFileName, forKey: sharedPhotoFilePathKey)
|
||||
userDefaults.set(fileName, forKey: sharedPhotoFileNameKey)
|
||||
|
||||
// Clean up any old base64 data that might exist
|
||||
userDefaults.removeObject(forKey: "sharedPhotoBase64")
|
||||
|
||||
userDefaults.synchronize()
|
||||
return true
|
||||
}
|
||||
|
||||
private func openMainApp() {
|
||||
// Open the main app with minimal URL - app will detect shared data on activation
|
||||
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
|
||||
}
|
||||
|
||||
// Fallback: use extension context
|
||||
extensionContext?.open(url, completionHandler: nil)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.timesafari</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -27,8 +27,8 @@
|
||||
"auto-run:android": "./scripts/auto-run.sh --platform=android",
|
||||
"auto-run:electron": "./scripts/auto-run.sh --platform=electron",
|
||||
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||
"build:capacitor:sync": "npm run build:capacitor && npx cap sync",
|
||||
"build:native": "vite build && npx cap sync && npx capacitor-assets generate",
|
||||
"build:capacitor:sync": "npm run build:capacitor && npx cap sync && node scripts/restore-local-plugins.js",
|
||||
"build:native": "vite build && npx cap sync && node scripts/restore-local-plugins.js && npx capacitor-assets generate",
|
||||
"assets:config": "npx tsx scripts/assets-config.ts",
|
||||
"assets:validate": "npx tsx scripts/assets-validator.ts",
|
||||
"assets:validate:android": "./scripts/build-android.sh --assets-only",
|
||||
@@ -156,7 +156,7 @@
|
||||
"@ethersproject/hdnode": "^5.7.0",
|
||||
"@ethersproject/wallet": "^5.8.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^7.1.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||
|
||||
60
scripts/README-restore-local-plugins.md
Normal file
60
scripts/README-restore-local-plugins.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Restore Local Capacitor Plugins
|
||||
|
||||
## Overview
|
||||
|
||||
The `restore-local-plugins.js` script ensures that local custom Capacitor plugins (`SafeArea` and `SharedImage`) are automatically restored to `android/app/src/main/assets/capacitor.plugins.json` after running `npx cap sync android`.
|
||||
|
||||
## Why This Is Needed
|
||||
|
||||
The `capacitor.plugins.json` file is auto-generated by Capacitor during `npx cap sync` and gets overwritten, removing any manually added local plugins. This script automatically restores them.
|
||||
|
||||
## Usage
|
||||
|
||||
### Automatic (Recommended)
|
||||
|
||||
The script is automatically run by:
|
||||
- `./scripts/build-android.sh` (after `cap sync`)
|
||||
- `npm run build:capacitor:sync`
|
||||
- `npm run build:native`
|
||||
|
||||
### Manual
|
||||
|
||||
If you run `npx cap sync android` directly, you can restore plugins manually:
|
||||
|
||||
```bash
|
||||
node scripts/restore-local-plugins.js
|
||||
```
|
||||
|
||||
## What It Does
|
||||
|
||||
1. Reads `android/app/src/main/assets/capacitor.plugins.json`
|
||||
2. Checks if local plugins (`SafeArea` and `SharedImage`) are present
|
||||
3. Adds any missing local plugins
|
||||
4. Preserves the existing JSON format
|
||||
|
||||
## Local Plugins
|
||||
|
||||
The following local plugins are automatically restored:
|
||||
|
||||
- **SafeArea**: `app.timesafari.safearea.SafeAreaPlugin`
|
||||
- **SharedImage**: `app.timesafari.sharedimage.SharedImagePlugin`
|
||||
|
||||
## Adding New Local Plugins
|
||||
|
||||
To add a new local plugin, edit `scripts/restore-local-plugins.js` and add it to the `LOCAL_PLUGINS` array:
|
||||
|
||||
```javascript
|
||||
const LOCAL_PLUGINS = [
|
||||
// ... existing plugins ...
|
||||
{
|
||||
pkg: 'YourPluginName',
|
||||
classpath: 'app.timesafari.yourpackage.YourPluginClass'
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The script is idempotent - running it multiple times won't create duplicates
|
||||
- The script preserves the existing JSON formatting (tabs, etc.)
|
||||
- If the plugins file doesn't exist, the script will exit with an error (run `npx cap sync android` first)
|
||||
@@ -385,6 +385,7 @@ fi
|
||||
if [ "$SYNC_ONLY" = true ]; then
|
||||
log_info "Sync-only mode: syncing with Capacitor"
|
||||
safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6
|
||||
safe_execute "Restoring local plugins" "node scripts/restore-local-plugins.js" || exit 7
|
||||
log_success "Sync completed successfully!"
|
||||
exit 0
|
||||
fi
|
||||
@@ -472,6 +473,9 @@ fi
|
||||
# Step 8: Sync with Capacitor
|
||||
safe_execute "Syncing with Capacitor" "npx cap sync android" || exit 6
|
||||
|
||||
# Step 8.5: Restore local plugins (capacitor.plugins.json gets overwritten by cap sync)
|
||||
safe_execute "Restoring local plugins" "node scripts/restore-local-plugins.js" || exit 7
|
||||
|
||||
# Step 9: Generate assets
|
||||
safe_execute "Generating assets" "npx capacitor-assets generate --android" || exit 7
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
78
scripts/restore-local-plugins.js
Executable file
78
scripts/restore-local-plugins.js
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Restore Local Capacitor Plugins
|
||||
*
|
||||
* This script ensures that local custom plugins (SafeArea and SharedImage)
|
||||
* are present in capacitor.plugins.json after `npx cap sync` runs.
|
||||
*
|
||||
* The capacitor.plugins.json file is auto-generated by Capacitor and gets
|
||||
* overwritten during sync, so we need to restore our local plugins.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/restore-local-plugins.js
|
||||
*
|
||||
* This should be run after `npx cap sync android` or `npx cap sync ios`
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const PLUGINS_FILE = path.join(__dirname, '../android/app/src/main/assets/capacitor.plugins.json');
|
||||
|
||||
// Local plugins that need to be added
|
||||
const LOCAL_PLUGINS = [
|
||||
{
|
||||
pkg: 'SafeArea',
|
||||
classpath: 'app.timesafari.safearea.SafeAreaPlugin'
|
||||
},
|
||||
{
|
||||
pkg: 'SharedImage',
|
||||
classpath: 'app.timesafari.sharedimage.SharedImagePlugin'
|
||||
}
|
||||
];
|
||||
|
||||
function restoreLocalPlugins() {
|
||||
try {
|
||||
// Read the current plugins file
|
||||
if (!fs.existsSync(PLUGINS_FILE)) {
|
||||
console.error(`❌ Plugins file not found: ${PLUGINS_FILE}`);
|
||||
console.error(' Run "npx cap sync android" first to generate the file.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(PLUGINS_FILE, 'utf8');
|
||||
let plugins = JSON.parse(content);
|
||||
|
||||
if (!Array.isArray(plugins)) {
|
||||
console.error(`❌ Invalid plugins file format: expected array, got ${typeof plugins}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check which local plugins are missing
|
||||
const existingPackages = new Set(plugins.map(p => p.pkg));
|
||||
const missingPlugins = LOCAL_PLUGINS.filter(p => !existingPackages.has(p.pkg));
|
||||
|
||||
if (missingPlugins.length === 0) {
|
||||
console.log('✅ All local plugins are already present in capacitor.plugins.json');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add missing plugins
|
||||
plugins.push(...missingPlugins);
|
||||
|
||||
// Write back to file with proper formatting (matching existing style)
|
||||
const formatted = JSON.stringify(plugins, null, '\t');
|
||||
fs.writeFileSync(PLUGINS_FILE, formatted + '\n', 'utf8');
|
||||
|
||||
console.log('✅ Restored local plugins to capacitor.plugins.json:');
|
||||
missingPlugins.forEach(p => {
|
||||
console.log(` - ${p.pkg} (${p.classpath})`);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Error restoring local plugins:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
restoreLocalPlugins();
|
||||
@@ -61,21 +61,10 @@ control over updates and validation * * @author Matthew Raymer */
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 mb-4">
|
||||
<button
|
||||
class="text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
|
||||
>
|
||||
<font-awesome icon="camera" />
|
||||
Add Photo
|
||||
</button>
|
||||
<!-- More Options Link -->
|
||||
<router-link
|
||||
:to="optionsRoute"
|
||||
class="block w-full text-center text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
|
||||
>
|
||||
More Options
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- Photo & More Options Link -->
|
||||
<router-link :to="photoOptionsRoute" :class="photoOptionsClasses">
|
||||
Photo & more options…
|
||||
</router-link>
|
||||
|
||||
<!-- Sign & Send Info -->
|
||||
<p class="text-center text-sm mb-4">
|
||||
@@ -229,6 +218,13 @@ export default class GiftDetailsStep extends Vue {
|
||||
private localAmount: number = 0;
|
||||
private localUnitCode: string = "HUR";
|
||||
|
||||
/**
|
||||
* CSS classes for the photo & more options link
|
||||
*/
|
||||
get photoOptionsClasses(): string {
|
||||
return "block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg mb-4";
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS classes for the cancel button
|
||||
*/
|
||||
@@ -308,7 +304,7 @@ export default class GiftDetailsStep extends Vue {
|
||||
/**
|
||||
* Computed route for photo & more options
|
||||
*/
|
||||
get optionsRoute(): RouteLocationRaw {
|
||||
get photoOptionsRoute(): RouteLocationRaw {
|
||||
return {
|
||||
name: "gifted-details",
|
||||
query: {
|
||||
|
||||
@@ -30,11 +30,15 @@
|
||||
|
||||
import { initializeApp } from "./main.common";
|
||||
import { App as CapacitorApp } from "@capacitor/app";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
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 { SharedImage } from "./plugins/SharedImagePlugin";
|
||||
import "./utils/safeAreaInset";
|
||||
|
||||
logger.log("[Capacitor] 🚀 Starting initialization");
|
||||
@@ -51,6 +55,55 @@ window.addEventListener("unhandledrejection", (event) => {
|
||||
|
||||
const deepLinkHandler = new DeepLinkHandler(router);
|
||||
|
||||
// Lock to prevent duplicate processing of shared images
|
||||
let isProcessingSharedImage = false;
|
||||
|
||||
/**
|
||||
* Stores shared image data in temp database
|
||||
* Handles clearing old data, converting base64 to data URL, and storing
|
||||
*
|
||||
* @param base64 - Raw base64 string of the image
|
||||
* @param fileName - Optional filename for logging
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async function storeSharedImageInTempDB(
|
||||
base64: string,
|
||||
fileName?: string,
|
||||
): Promise<void> {
|
||||
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
|
||||
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],
|
||||
);
|
||||
|
||||
logger.info(`[Main] Stored shared image: ${fileName || "unknown"}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles deep link routing for the application
|
||||
* Processes URLs in the format timesafari://<route>/<param>
|
||||
@@ -67,11 +120,142 @@ const deepLinkHandler = new DeepLinkHandler(router);
|
||||
*
|
||||
* @throws {Error} If URL format is invalid
|
||||
*/
|
||||
/**
|
||||
* Check for native shared image using SharedImage plugin
|
||||
* Reads from native layer (App Group UserDefaults on iOS, SharedPreferences on Android)
|
||||
* and stores in temp database before routing to shared-photo view
|
||||
*/
|
||||
async function checkAndStoreNativeSharedImage(): Promise<{
|
||||
success: boolean;
|
||||
fileName?: string;
|
||||
}> {
|
||||
// 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" &&
|
||||
Capacitor.getPlatform() !== "android")
|
||||
) {
|
||||
isProcessingSharedImage = false;
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// Use SharedImage plugin to get shared image data directly from native layer
|
||||
// No file I/O or polling needed - direct native-to-JS communication
|
||||
let result;
|
||||
try {
|
||||
result = await SharedImage.getSharedImage();
|
||||
} catch (error) {
|
||||
logger.error("[Main] Error calling SharedImage.getSharedImage():", {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
isProcessingSharedImage = false;
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// Check if we have valid image data (base64 must be non-null and non-empty)
|
||||
if (result && result.base64 && result.base64.trim().length > 0) {
|
||||
const fileName = result.fileName || "shared-image.jpg";
|
||||
|
||||
// Store in temp database using extracted method
|
||||
logger.info(
|
||||
"[Main] Native shared image found (via plugin), storing in temp DB",
|
||||
);
|
||||
await storeSharedImageInTempDB(result.base64, fileName);
|
||||
|
||||
isProcessingSharedImage = false;
|
||||
return { success: true, fileName };
|
||||
}
|
||||
|
||||
// No shared image found
|
||||
logger.debug("[Main] No shared image found via plugin");
|
||||
isProcessingSharedImage = false;
|
||||
return { success: false };
|
||||
} catch (error) {
|
||||
logger.error("[Main] Error checking for native shared image:", error);
|
||||
isProcessingSharedImage = false;
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeepLink = async (data: { url: string }) => {
|
||||
const { url } = data;
|
||||
logger.debug(`[Main] 🌐 Deeplink received from Capacitor: ${url}`);
|
||||
|
||||
try {
|
||||
// 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 (
|
||||
isEmptyPathUrl &&
|
||||
Capacitor.isNativePlatform() &&
|
||||
Capacitor.getPlatform() === "ios"
|
||||
) {
|
||||
logger.debug(
|
||||
"[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
|
||||
// The checkAndStoreNativeSharedImage function now uses polling internally, so we just call it once
|
||||
try {
|
||||
const imageResult = await checkAndStoreNativeSharedImage();
|
||||
|
||||
if (imageResult.success) {
|
||||
logger.info(
|
||||
"[Main] ✅ Native shared image found, navigating to shared-photo",
|
||||
);
|
||||
|
||||
// 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, 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);
|
||||
// If check fails, don't process as deep link (empty path would fail validation anyway)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for router to be ready
|
||||
logger.debug(`[Main] ⏳ Waiting for router to be ready...`);
|
||||
await router.isReady();
|
||||
@@ -159,10 +343,96 @@ 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" && Capacitor.getPlatform() !== "android")
|
||||
) {
|
||||
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 sheet)
|
||||
// On Android, share intents are processed in MainActivity.onCreate, so we need to check
|
||||
// after a delay to ensure the native code has finished processing
|
||||
if (
|
||||
Capacitor.isNativePlatform() &&
|
||||
(Capacitor.getPlatform() === "ios" || Capacitor.getPlatform() === "android")
|
||||
) {
|
||||
// Use multiple checks with increasing delays to handle timing issues
|
||||
// Android share intent processing happens in onCreate, which may complete after JS loads
|
||||
const checkDelays =
|
||||
Capacitor.getPlatform() === "android"
|
||||
? [500, 1500, 3000] // Android needs more time for share intent processing
|
||||
: [1000]; // iOS is faster
|
||||
|
||||
checkDelays.forEach((delay) => {
|
||||
setTimeout(async () => {
|
||||
await checkForSharedImageAndNavigate();
|
||||
}, delay);
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for app state changes to detect when app becomes active
|
||||
if (
|
||||
Capacitor.isNativePlatform() &&
|
||||
(Capacitor.getPlatform() === "ios" || Capacitor.getPlatform() === "android")
|
||||
) {
|
||||
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 {
|
||||
|
||||
15
src/plugins/SharedImagePlugin.ts
Normal file
15
src/plugins/SharedImagePlugin.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* SharedImage Capacitor Plugin
|
||||
* Provides access to shared image data from native Share Extension/Intent
|
||||
*/
|
||||
|
||||
import { registerPlugin } from "@capacitor/core";
|
||||
import type { SharedImagePlugin } from "./definitions";
|
||||
|
||||
const SharedImage = registerPlugin<SharedImagePlugin>("SharedImage", {
|
||||
web: () =>
|
||||
import("./SharedImagePlugin.web").then((m) => new m.SharedImagePluginWeb()),
|
||||
});
|
||||
|
||||
export * from "./definitions";
|
||||
export { SharedImage };
|
||||
21
src/plugins/SharedImagePlugin.web.ts
Normal file
21
src/plugins/SharedImagePlugin.web.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Web implementation of SharedImagePlugin
|
||||
* Returns null/false for web platform (no native sharing support)
|
||||
*/
|
||||
|
||||
import { WebPlugin } from "@capacitor/core";
|
||||
import type { SharedImagePlugin, SharedImageResult } from "./definitions";
|
||||
|
||||
export class SharedImagePluginWeb
|
||||
extends WebPlugin
|
||||
implements SharedImagePlugin
|
||||
{
|
||||
async getSharedImage(): Promise<SharedImageResult | null> {
|
||||
// Web platform doesn't support native sharing
|
||||
return null;
|
||||
}
|
||||
|
||||
async hasSharedImage(): Promise<{ hasImage: boolean }> {
|
||||
return { hasImage: false };
|
||||
}
|
||||
}
|
||||
23
src/plugins/definitions.ts
Normal file
23
src/plugins/definitions.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Type definitions for SharedImage plugin
|
||||
*/
|
||||
|
||||
export interface SharedImageResult {
|
||||
base64: string;
|
||||
fileName: string;
|
||||
}
|
||||
|
||||
export interface SharedImagePlugin {
|
||||
/**
|
||||
* Get shared image data from native layer
|
||||
* Returns base64 string and fileName, or null if no image exists
|
||||
* Clears the data after reading to prevent re-reading
|
||||
*/
|
||||
getSharedImage(): Promise<SharedImageResult | null>;
|
||||
|
||||
/**
|
||||
* Check if shared image exists without reading it
|
||||
* Useful for quick checks before calling getSharedImage()
|
||||
*/
|
||||
hasSharedImage(): Promise<{ hasImage: boolean }>;
|
||||
}
|
||||
@@ -108,60 +108,37 @@ Raymer * @version 1.0.0 */
|
||||
/>
|
||||
|
||||
<div v-if="isUserRegistered" id="sectionRecordSomethingGiven">
|
||||
<!-- Record Quick-Action - variant A -->
|
||||
<div v-if="false" class="mb-6">
|
||||
<div class="flex justify-end gap-1">
|
||||
<!-- Record Quick-Action -->
|
||||
<div class="mb-6">
|
||||
<div class="flex gap-2 items-center mb-2">
|
||||
<h2 class="font-bold">Record something given by:</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="grow text-center text-xl font-bold bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
|
||||
@click="openPersonDialog()"
|
||||
>
|
||||
<font-awesome icon="circle-plus" />
|
||||
Record a Give
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-center text-lg uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
|
||||
@click="openProjectDialog()"
|
||||
>
|
||||
<font-awesome icon="folder-open" class="fa-fw" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-center text-lg uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
|
||||
class="block ms-auto text-sm text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-1.5 rounded-full"
|
||||
@click="openGiftedPrompts()"
|
||||
>
|
||||
<font-awesome icon="lightbulb" class="fa-fw" />
|
||||
<font-awesome
|
||||
icon="lightbulb"
|
||||
class="block text-center w-[1em]"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Record Quick-Action - variant B -->
|
||||
<div class="mb-6">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full text-center text-2xl font-bold bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg mb-2"
|
||||
@click="openPersonDialog()"
|
||||
>
|
||||
<font-awesome icon="circle-plus" />
|
||||
Record a Give
|
||||
</button>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-center text-base bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
|
||||
class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
|
||||
@click="openPersonDialog()"
|
||||
>
|
||||
<font-awesome icon="user" />
|
||||
Person
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
|
||||
@click="openProjectDialog()"
|
||||
>
|
||||
<font-awesome icon="folder-open" />
|
||||
Project Give
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-center text-base bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
|
||||
@click="openGiftedPrompts()"
|
||||
>
|
||||
<font-awesome icon="lightbulb" />
|
||||
Give Ideas
|
||||
Project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import axios from "axios";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Component, Vue, Watch } from "vue-facing-decorator";
|
||||
import {
|
||||
RouteLocationNormalizedLoaded,
|
||||
RouteLocationRaw,
|
||||
@@ -191,7 +191,30 @@ export default class SharedPhotoView extends Vue {
|
||||
*/
|
||||
async mounted() {
|
||||
this.notify = createNotifyHelpers(this.$notify);
|
||||
await this.loadSharedImage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Watches for route query changes to reload image when navigating
|
||||
* to the same route with different query parameters (e.g., new fileName or _refresh)
|
||||
* This handles both new navigations and refreshes when already on the route
|
||||
*/
|
||||
@Watch("$route.query", { deep: true })
|
||||
async onRouteQueryChange() {
|
||||
// Reload image when any query parameter changes (fileName or _refresh)
|
||||
// This ensures the image refreshes when a new image is shared while already on this view
|
||||
await this.loadSharedImage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the shared image from temporary storage
|
||||
*
|
||||
* Retrieves the shared image data from the temp database, converts it to a blob,
|
||||
* and updates component state. Cleans up temporary storage after successful loading.
|
||||
*
|
||||
* @async
|
||||
*/
|
||||
private async loadSharedImage() {
|
||||
try {
|
||||
// Get activeDid from active_identity table (single source of truth)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -209,6 +232,9 @@ export default class SharedPhotoView extends Vue {
|
||||
this.imageFileName = this.$route.query["fileName"] as string;
|
||||
} else {
|
||||
logger.error("No appropriate image found in temp storage.", temp);
|
||||
// Clear image state if no temp data found
|
||||
this.imageBlob = undefined;
|
||||
this.imageFileName = undefined;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
logger.error("Got an error loading an identifier:", err);
|
||||
@@ -216,6 +242,9 @@ export default class SharedPhotoView extends Vue {
|
||||
NOTIFY_SHARED_PHOTO_LOAD_ERROR.message,
|
||||
TIMEOUTS.STANDARD,
|
||||
);
|
||||
// Clear image state on error
|
||||
this.imageBlob = undefined;
|
||||
this.imageFileName = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user