forked from trent_larson/crowd-funder-for-time-pwa
feat(ios): implement native share target for images
Implement iOS Share Extension to enable native image sharing from Photos and other apps directly into TimeSafari. Users can now share images from the iOS share sheet, which will open in SharedPhotoView for use as gifts or profile pictures. iOS Native Implementation: - Add TimeSafariShareExtension target with ShareViewController - Configure App Groups for data sharing between extension and main app - Implement ShareViewController to process shared images and convert to base64 - Store shared image data in App Group UserDefaults - Add ShareImageBridge utility to read shared data from App Group - Update AppDelegate to handle shared-photo deep link and bridge data to JS JavaScript Integration: - Add checkAndStoreNativeSharedImage() in main.capacitor.ts to read shared images from native layer via temporary file bridge - Convert base64 data to data URL format for compatibility with base64ToBlob - Integrate with existing SharedPhotoView component - Add "shared-photo" to deep link validation schema Build System: - Integrate Xcode 26 / CocoaPods compatibility workaround into build-ios.sh - Add run_pod_install_with_workaround() for explicit pod install - Add run_cap_sync_with_workaround() for Capacitor sync (which runs pod install internally) - Automatically detect project format version 70 and apply workaround - Remove standalone pod-install-workaround.sh script Code Cleanup: - Remove verbose debug logs from ShareViewController, AppDelegate, and main.capacitor.ts - Retain essential logger calls for production debugging Documentation: - Add ios-share-extension-setup.md with manual Xcode setup instructions - Add ios-share-extension-git-commit-guide.md for version control best practices - Add ios-share-implementation-status.md tracking implementation progress - Add native-share-target-implementation.md with overall architecture - Add xcode-26-cocoapods-workaround.md documenting the compatibility issue The implementation uses a temporary file bridge (AppDelegate writes to Documents directory, JS reads via Capacitor Filesystem plugin) as a workaround for Capacitor plugin auto-discovery issues. This can be improved in the future by properly registering ShareImagePlugin in Capacitor's plugin registry.
This commit is contained in:
@@ -30,11 +30,15 @@
|
||||
|
||||
import { initializeApp } from "./main.common";
|
||||
import { App as CapacitorApp } from "@capacitor/app";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
||||
import router from "./router";
|
||||
import { handleApiError } from "./services/api";
|
||||
import { AxiosError } from "axios";
|
||||
import { DeepLinkHandler } from "./services/deepLinks";
|
||||
import { logger, safeStringify } from "./utils/logger";
|
||||
import { PlatformServiceFactory } from "./services/PlatformServiceFactory";
|
||||
import { SHARED_PHOTO_BASE64_KEY } from "./libs/util";
|
||||
import "./utils/safeAreaInset";
|
||||
|
||||
logger.log("[Capacitor] 🚀 Starting initialization");
|
||||
@@ -67,11 +71,220 @@ const deepLinkHandler = new DeepLinkHandler(router);
|
||||
*
|
||||
* @throws {Error} If URL format is invalid
|
||||
*/
|
||||
/**
|
||||
* Check for native shared image from iOS App Group UserDefaults
|
||||
* and store in temp database before routing to shared-photo view
|
||||
*/
|
||||
async function checkAndStoreNativeSharedImage(): Promise<{
|
||||
success: boolean;
|
||||
fileName?: string;
|
||||
}> {
|
||||
if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== "ios") {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug("[Main] Checking for iOS shared image from App Group");
|
||||
|
||||
// Use Capacitor's native bridge to call the ShareImagePlugin
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const capacitor = (window as any).Capacitor;
|
||||
|
||||
if (!capacitor || !capacitor.Plugins) {
|
||||
logger.debug("[Main] Capacitor plugins not available");
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// WORKAROUND: Since the plugin isn't being auto-discovered, use a temp file bridge
|
||||
// AppDelegate writes the shared image data to a temp file, and we read it here
|
||||
try {
|
||||
const tempFilePath = "timesafari_shared_photo.json";
|
||||
const fileContent = await Filesystem.readFile({
|
||||
path: tempFilePath,
|
||||
directory: Directory.Documents,
|
||||
encoding: Encoding.UTF8,
|
||||
});
|
||||
|
||||
if (fileContent.data) {
|
||||
const sharedData = JSON.parse(fileContent.data as string);
|
||||
const base64 = sharedData.base64;
|
||||
const fileName = sharedData.fileName || "shared-image.jpg";
|
||||
|
||||
if (base64) {
|
||||
// Store in temp database using dbExec directly
|
||||
logger.info(
|
||||
"[Main] Native shared image found (via temp file), storing in temp DB",
|
||||
);
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
|
||||
// Convert raw base64 to data URL format that base64ToBlob expects
|
||||
// base64ToBlob expects format: "data:image/jpeg;base64,/9j/4AAQ..."
|
||||
// Try to detect image type from base64 or default to jpeg
|
||||
let mimeType = "image/jpeg"; // default
|
||||
if (base64.startsWith("/9j/") || base64.startsWith("iVBORw0KGgo")) {
|
||||
// JPEG or PNG
|
||||
mimeType = base64.startsWith("/9j/") ? "image/jpeg" : "image/png";
|
||||
}
|
||||
const dataUrl = `data:${mimeType};base64,${base64}`;
|
||||
|
||||
// Use INSERT OR REPLACE to handle existing records
|
||||
await platformService.dbExec(
|
||||
"INSERT OR REPLACE INTO temp (id, blobB64) VALUES (?, ?)",
|
||||
[SHARED_PHOTO_BASE64_KEY, dataUrl],
|
||||
);
|
||||
|
||||
// Delete the temp file
|
||||
try {
|
||||
await Filesystem.deleteFile({
|
||||
path: tempFilePath,
|
||||
directory: Directory.Documents,
|
||||
});
|
||||
} catch (deleteError) {
|
||||
logger.error("[Main] Failed to delete temp file:", deleteError);
|
||||
}
|
||||
|
||||
logger.info(`[Main] Stored shared image: ${fileName}`);
|
||||
return { success: true, fileName };
|
||||
}
|
||||
}
|
||||
} catch (fileError: unknown) {
|
||||
// File doesn't exist or can't be read - that's okay, try plugin method
|
||||
logger.debug(
|
||||
"[Main] Temp file not found or unreadable (this is normal if plugin works)",
|
||||
);
|
||||
}
|
||||
|
||||
// NOTE: Plugin registration issue - ShareImage plugin is not being auto-discovered
|
||||
// This is a known issue that needs to be resolved. For now, we use the temp file workaround above.
|
||||
|
||||
// Try multiple methods to call the plugin (fallback if temp file method fails)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const plugins = (capacitor as any).Plugins;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const shareImagePlugin = (plugins as any)?.ShareImage;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let result: any = null;
|
||||
|
||||
if (
|
||||
shareImagePlugin &&
|
||||
typeof shareImagePlugin.getSharedImageData === "function"
|
||||
) {
|
||||
logger.debug("[Main] Using direct plugin method");
|
||||
try {
|
||||
result = await shareImagePlugin.getSharedImageData();
|
||||
} catch (pluginError) {
|
||||
logger.error("[Main] Plugin call failed:", pluginError);
|
||||
return { success: false };
|
||||
}
|
||||
} else {
|
||||
// Method 2: Use Capacitor's execute method
|
||||
logger.debug(
|
||||
"[Main] Plugin not found directly, trying Capacitor.execute",
|
||||
);
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const bridge = (capacitor as any).getBridge?.();
|
||||
if (bridge) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
result = await bridge.execute({
|
||||
pluginId: "ShareImage",
|
||||
methodName: "getSharedImageData",
|
||||
options: {},
|
||||
});
|
||||
} else {
|
||||
// Method 3: Try execute on Plugins object
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((plugins as any)?.execute) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
result = await (plugins as any).execute(
|
||||
"ShareImage",
|
||||
"getSharedImageData",
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (executeError) {
|
||||
logger.error("[Main] Execute method failed:", executeError);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
if (!result || !result.success || !result.data) {
|
||||
logger.debug("[Main] No shared image data found in result");
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const { base64, fileName } = result.data;
|
||||
|
||||
if (!base64) {
|
||||
logger.debug("[Main] Shared image data missing base64");
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
logger.info("[Main] Native shared image found, storing in temp DB");
|
||||
|
||||
// Store in temp database (similar to web flow)
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
// $insertEntity is available via PlatformServiceMixin
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await (platformService as any).$insertEntity(
|
||||
"temp",
|
||||
{ id: SHARED_PHOTO_BASE64_KEY, blobB64: base64 },
|
||||
["id", "blobB64"],
|
||||
);
|
||||
|
||||
logger.info(`[Main] Stored shared image: ${fileName || "unknown"}`);
|
||||
return { success: true, fileName: fileName || "shared-image.jpg" };
|
||||
} catch (error) {
|
||||
logger.error("[Main] Error checking for native shared image:", error);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeepLink = async (data: { url: string }) => {
|
||||
const { url } = data;
|
||||
logger.debug(`[Main] 🌐 Deeplink received from Capacitor: ${url}`);
|
||||
|
||||
try {
|
||||
// Check if this is a shared-photo deep link from native share
|
||||
const isSharedPhotoLink = url.includes("timesafari://shared-photo");
|
||||
|
||||
if (
|
||||
isSharedPhotoLink &&
|
||||
Capacitor.isNativePlatform() &&
|
||||
Capacitor.getPlatform() === "ios"
|
||||
) {
|
||||
logger.debug(
|
||||
"[Main] 📸 Shared photo deep link detected, checking for native shared image",
|
||||
);
|
||||
|
||||
// Try to get shared image from App Group and store in temp database
|
||||
try {
|
||||
const imageResult = await checkAndStoreNativeSharedImage();
|
||||
|
||||
if (imageResult.success) {
|
||||
logger.info("[Main] ✅ Native shared image stored in temp database");
|
||||
|
||||
// Add fileName to the URL as a query parameter if we have it
|
||||
if (imageResult.fileName) {
|
||||
const urlObj = new URL(url);
|
||||
urlObj.searchParams.set("fileName", imageResult.fileName);
|
||||
const modifiedUrl = urlObj.toString();
|
||||
data.url = modifiedUrl;
|
||||
logger.debug(`[Main] Added fileName to URL: ${modifiedUrl}`);
|
||||
}
|
||||
} else {
|
||||
logger.debug(
|
||||
"[Main] ℹ️ No native shared image found (may be from web or already processed)",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("[Main] Error processing native shared image:", error);
|
||||
// Continue with normal deep link processing even if native check fails
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for router to be ready
|
||||
logger.debug(`[Main] ⏳ Waiting for router to be ready...`);
|
||||
await router.isReady();
|
||||
|
||||
Reference in New Issue
Block a user