refactor(shared-image): replace temp file approach with native Capacitor plugins
Replace the buggy temp file polling mechanism with direct native-to-JS communication via custom Capacitor plugins for iOS and Android. This eliminates race conditions, file management complexity, and polling overhead. BREAKING CHANGE: Removes backward compatibility with temp file approach. Android minSdkVersion upgraded from 22 to 23. Changes: - iOS: Created SharedImagePlugin (CAPBridgedPlugin) and SharedImageUtility - Uses App Group UserDefaults for data sharing between extension and app - Implements getSharedImage() and hasSharedImage() methods - Removes temp file writing/reading logic from AppDelegate - Android: Created SharedImagePlugin with @CapacitorPlugin annotation - Uses SharedPreferences for data storage instead of temp files - Implements same interface as iOS plugin - Removes temp file handling from MainActivity - TypeScript: Added plugin definitions and registration - Created SharedImagePlugin interface and web fallback - Updated main.capacitor.ts to use plugin instead of Filesystem API - Removed pollForFileExistence() and related file I/O code - Android: Upgraded minSdkVersion from 22 to 23 - Required for SharedPreferences improvements and better API support Benefits: - Eliminates race conditions from file polling - Removes file cleanup complexity - Direct native-to-JS communication (no file I/O) - Better performance (no polling overhead) - More reliable data transfer between share extension and app - Cleaner architecture with proper plugin abstraction
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
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 */
|
||||
@@ -55,6 +57,8 @@
|
||||
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 */
|
||||
@@ -123,6 +127,8 @@
|
||||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C8C56E152EE064CA00737D0E /* SharedImagePlugin.swift */,
|
||||
C8C56E132EE0474B00737D0E /* SharedImageUtility.swift */,
|
||||
C86585E52ED4577F00824752 /* App.entitlements */,
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
@@ -339,7 +345,9 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C8C56E162EE064CB00737D0E /* SharedImagePlugin.swift in Sources */,
|
||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
|
||||
C8C56E142EE0474B00737D0E /* SharedImageUtility.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
@@ -40,47 +80,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
/**
|
||||
* 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() {
|
||||
let appGroupIdentifier = "group.app.timesafari"
|
||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if shared photo is ready
|
||||
if userDefaults.bool(forKey: "sharedPhotoReady") {
|
||||
if SharedImageUtility.isSharedPhotoReady() {
|
||||
// Clear the flag
|
||||
userDefaults.removeObject(forKey: "sharedPhotoReady")
|
||||
userDefaults.synchronize()
|
||||
|
||||
// Get and process shared image data
|
||||
if let sharedData = getSharedImageData() {
|
||||
writeSharedImageToTempFile(sharedData)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write shared image data to temp file for JavaScript to read
|
||||
*/
|
||||
private func writeSharedImageToTempFile(_ sharedData: [String: String]) {
|
||||
let fileManager = FileManager.default
|
||||
guard let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
return
|
||||
}
|
||||
|
||||
let tempFileURL = documentsDir.appendingPathComponent("timesafari_shared_photo.json")
|
||||
|
||||
let jsonData: [String: String] = [
|
||||
"base64": sharedData["base64"] ?? "",
|
||||
"fileName": sharedData["fileName"] ?? ""
|
||||
]
|
||||
|
||||
if let json = try? JSONSerialization.data(withJSONObject: jsonData, options: []) {
|
||||
try? json.write(to: tempFileURL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,35 +99,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
|
||||
// Check if this is a shared-photo deep link and store image data in a way JS can access
|
||||
if url.scheme == "timesafari" && url.host == "shared-photo" {
|
||||
// Try to get shared image from App Group and store it in a temp file that JS can read
|
||||
// This is a workaround until the plugin is properly registered
|
||||
if let sharedData = getSharedImageData() {
|
||||
// Write to a temp file in the app's Documents directory that JavaScript can read via Filesystem plugin
|
||||
let fileManager = FileManager.default
|
||||
if let documentsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
|
||||
let tempFileURL = documentsDir.appendingPathComponent("timesafari_shared_photo.json")
|
||||
|
||||
// Create JSON data
|
||||
let jsonData: [String: String] = [
|
||||
"base64": sharedData["base64"] ?? "",
|
||||
"fileName": sharedData["fileName"] ?? ""
|
||||
]
|
||||
|
||||
if let json = try? JSONSerialization.data(withJSONObject: jsonData, options: []) {
|
||||
do {
|
||||
try json.write(to: tempFileURL)
|
||||
} catch {
|
||||
// Error writing temp file - will be handled by JS layer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Called when the app was launched with a url. Feel free to add additional processing here,
|
||||
// but if you want the App API to support tracking app url opens, make sure to keep this call
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -128,29 +113,5 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for shared image from Share Extension
|
||||
* Reads from App Group UserDefaults and returns shared image data if available
|
||||
*
|
||||
* @returns Dictionary with "base64" and "fileName" keys, or nil if no shared image
|
||||
*/
|
||||
func getSharedImageData() -> [String: String]? {
|
||||
let appGroupIdentifier = "group.app.timesafari"
|
||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let base64 = userDefaults.string(forKey: "sharedPhotoBase64"),
|
||||
let fileName = userDefaults.string(forKey: "sharedPhotoFileName") else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear the shared data after reading
|
||||
userDefaults.removeObject(forKey: "sharedPhotoBase64")
|
||||
userDefaults.removeObject(forKey: "sharedPhotoFileName")
|
||||
userDefaults.synchronize()
|
||||
|
||||
return ["base64": base64, "fileName": fileName]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
82
ios/App/App/SharedImageUtility.swift
Normal file
82
ios/App/App/SharedImageUtility.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// SharedImageUtility.swift
|
||||
// App
|
||||
//
|
||||
// Shared utility for accessing shared image data from App Group UserDefaults
|
||||
// Used by both AppDelegate and SharedImagePlugin
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class SharedImageUtility {
|
||||
private static let appGroupIdentifier = "group.app.timesafari"
|
||||
private static let sharedPhotoBase64Key = "sharedPhotoBase64"
|
||||
private static let sharedPhotoFileNameKey = "sharedPhotoFileName"
|
||||
private static let sharedPhotoReadyKey = "sharedPhotoReady"
|
||||
|
||||
/**
|
||||
* Get shared image data from App Group UserDefaults
|
||||
* 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
|
||||
}
|
||||
|
||||
guard let base64 = userDefaults.string(forKey: sharedPhotoBase64Key),
|
||||
let fileName = userDefaults.string(forKey: sharedPhotoFileNameKey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clear the shared data after reading
|
||||
userDefaults.removeObject(forKey: sharedPhotoBase64Key)
|
||||
userDefaults.removeObject(forKey: sharedPhotoFileNameKey)
|
||||
userDefaults.synchronize()
|
||||
|
||||
return ["base64": base64, "fileName": fileName]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if shared image exists without reading it
|
||||
*
|
||||
* @returns true if shared image data exists, false otherwise
|
||||
*/
|
||||
static func hasSharedImage() -> Bool {
|
||||
guard let userDefaults = UserDefaults(suiteName: appGroupIdentifier) else {
|
||||
return false
|
||||
}
|
||||
|
||||
return userDefaults.string(forKey: sharedPhotoBase64Key) != nil &&
|
||||
userDefaults.string(forKey: sharedPhotoFileNameKey) != nil
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user