diff --git a/assets/lang/strings.ts b/assets/lang/strings.ts
index 7a5d88978..6962db664 100644
--- a/assets/lang/strings.ts
+++ b/assets/lang/strings.ts
@@ -322,6 +322,14 @@ const translations = {
action: 'Export backup key',
},
},
+ ShareExtension: {
+ title: 'Save to Internxt',
+ notSignedIn: {
+ title: "You're not signed in",
+ subtitle: 'Log in to upload files securely',
+ openLogin: 'Open Internxt login',
+ },
+ },
},
buttons: {
fixPhoto: 'Repair photo',
@@ -1126,6 +1134,14 @@ const translations = {
action: 'Exportar la clave de seguridad',
},
},
+ ShareExtension: {
+ title: 'Guardar en Internxt',
+ notSignedIn: {
+ title: 'No has iniciado sesión',
+ subtitle: 'Inicia sesión para subir archivos de forma segura',
+ openLogin: 'Abrir login de Internxt',
+ },
+ },
},
buttons: {
fixPhoto: 'Reparar foto',
diff --git a/ios/Internxt.xcodeproj/project.pbxproj b/ios/Internxt.xcodeproj/project.pbxproj
index 02945904f..8ae710daa 100644
--- a/ios/Internxt.xcodeproj/project.pbxproj
+++ b/ios/Internxt.xcodeproj/project.pbxproj
@@ -270,13 +270,13 @@
LastUpgradeCheck = 1130;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
+ DevelopmentTeam = JR4S3SY396;
LastSwiftMigration = 1250;
- DevelopmentTeam = "JR4S3SY396";
ProvisioningStyle = Automatic;
};
573EC8782D084646801534FA = {
+ DevelopmentTeam = JR4S3SY396;
LastSwiftMigration = 1250;
- DevelopmentTeam = "JR4S3SY396";
ProvisioningStyle = Automatic;
};
};
@@ -607,7 +607,10 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = InternxtShareExtension/InternxtShareExtension.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = JR4S3SY396;
ENABLE_ON_DEMAND_RESOURCES = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = InternxtShareExtension/Info.plist;
@@ -624,9 +627,6 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
- DEVELOPMENT_TEAM = "JR4S3SY396";
- CODE_SIGN_IDENTITY = "Apple Development";
- CODE_SIGN_STYLE = Automatic;
};
name = Debug;
};
@@ -637,7 +637,10 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Internxt/Internxt.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = JR4S3SY396;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
@@ -663,9 +666,6 @@
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
- DEVELOPMENT_TEAM = "JR4S3SY396";
- CODE_SIGN_IDENTITY = "Apple Development";
- CODE_SIGN_STYLE = Automatic;
};
name = Debug;
};
@@ -676,7 +676,10 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Internxt/Internxt.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = JR4S3SY396;
INFOPLIST_FILE = Internxt/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
LD_RUNPATH_SEARCH_PATHS = (
@@ -696,9 +699,6 @@
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
- DEVELOPMENT_TEAM = "JR4S3SY396";
- CODE_SIGN_IDENTITY = "Apple Development";
- CODE_SIGN_STYLE = Automatic;
};
name = Release;
};
@@ -826,7 +826,10 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = InternxtShareExtension/InternxtShareExtension.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = JR4S3SY396;
ENABLE_ON_DEMAND_RESOURCES = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = InternxtShareExtension/Info.plist;
@@ -842,9 +845,6 @@
PRODUCT_NAME = InternxtShareExtension;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
- DEVELOPMENT_TEAM = "JR4S3SY396";
- CODE_SIGN_IDENTITY = "Apple Development";
- CODE_SIGN_STYLE = Automatic;
};
name = Release;
};
diff --git a/ios/Internxt/AppDelegate.swift b/ios/Internxt/AppDelegate.swift
index a7887e1e5..0ec1e60c0 100644
--- a/ios/Internxt/AppDelegate.swift
+++ b/ios/Internxt/AppDelegate.swift
@@ -1,6 +1,7 @@
import Expo
import React
import ReactAppDependencyProvider
+import Security
@UIApplicationMain
public class AppDelegate: ExpoAppDelegate {
@@ -29,9 +30,100 @@ public class AppDelegate: ExpoAppDelegate {
launchOptions: launchOptions)
#endif
+ syncAuthStatusToAppGroup()
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
+ // Sync auth status whenever the app moves to background so the share
+ // extension always reads an up-to-date value from the shared UserDefaults.
+ public override func applicationDidEnterBackground(_ application: UIApplication) {
+ syncAuthStatusToAppGroup()
+ super.applicationDidEnterBackground(application)
+ }
+
+ // MARK: - App Group auth sync
+
+ private func syncAuthStatusToAppGroup() {
+ guard let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String,
+ let defaults = UserDefaults(suiteName: appGroup),
+ let sharedGroup = Bundle.main.object(forInfoDictionaryKey: "SharedKeychainGroup") as? String
+ else { return }
+
+ let isAuthenticated = privateKeychainItemExists(key: "photosToken")
+ defaults.set(isAuthenticated, forKey: "isAuthenticated")
+
+ if isAuthenticated {
+ copyToSharedKeychain(privateKey: "photosToken", sharedKey: "shared_photosToken", accessGroup: sharedGroup)
+ copyToSharedKeychain(privateKey: "xUser_mnemonic", sharedKey: "shared_mnemonic", accessGroup: sharedGroup)
+ defaults.set(readEmailFromKeychain(), forKey: "userEmail")
+ } else {
+ deleteFromSharedKeychain(key: "shared_photosToken", accessGroup: sharedGroup)
+ deleteFromSharedKeychain(key: "shared_mnemonic", accessGroup: sharedGroup)
+ defaults.removeObject(forKey: "userEmail")
+ }
+ }
+
+ private func readEmailFromKeychain() -> String? {
+ guard let data = readFromPrivateKeychain(key: "xUser_data"),
+ var raw = String(data: data, encoding: .utf8) else { return nil }
+ if raw.hasPrefix("\"") && raw.hasSuffix("\"") {
+ raw = String(raw.dropFirst().dropLast())
+ }
+ guard let jsonData = raw.data(using: .utf8),
+ let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
+ let email = json["email"] as? String else { return nil }
+ return email
+ }
+
+ private func privateKeychainItemExists(key: String) -> Bool {
+ return readFromPrivateKeychain(key: key) != nil
+ }
+
+ private func copyToSharedKeychain(privateKey: String, sharedKey: String, accessGroup: String) {
+ guard let data = readFromPrivateKeychain(key: privateKey) else { return }
+ writeToSharedKeychain(data: data, key: sharedKey, accessGroup: accessGroup)
+ }
+
+ private func readFromPrivateKeychain(key: String) -> Data? {
+ var result: AnyObject?
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "app:no-auth",
+ kSecAttrGeneric as String: Data(key.utf8),
+ kSecAttrAccount as String: Data(key.utf8),
+ kSecMatchLimit as String: kSecMatchLimitOne,
+ kSecReturnData as String: true,
+ ]
+ guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
+ let data = result as? Data else { return nil }
+ return data
+ }
+
+ private func writeToSharedKeychain(data: Data, key: String, accessGroup: String) {
+ deleteFromSharedKeychain(key: key, accessGroup: accessGroup)
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "app:no-auth",
+ kSecAttrGeneric as String: Data(key.utf8),
+ kSecAttrAccount as String: Data(key.utf8),
+ kSecAttrAccessGroup as String: accessGroup,
+ kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked,
+ kSecValueData as String: data,
+ ]
+ SecItemAdd(query as CFDictionary, nil)
+ }
+
+ private func deleteFromSharedKeychain(key: String, accessGroup: String) {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "app:no-auth",
+ kSecAttrGeneric as String: Data(key.utf8),
+ kSecAttrAccount as String: Data(key.utf8),
+ kSecAttrAccessGroup as String: accessGroup,
+ ]
+ SecItemDelete(query as CFDictionary)
+ }
+
// Linking API
public override func application(
_ app: UIApplication,
diff --git a/ios/Internxt/Info.plist b/ios/Internxt/Info.plist
index a25689bfd..b4f15379e 100644
--- a/ios/Internxt/Info.plist
+++ b/ios/Internxt/Info.plist
@@ -6,6 +6,8 @@
group.com.internxt.snacks
AppGroupIdentifier
group.com.internxt.snacks
+ SharedKeychainGroup
+ $(AppIdentifierPrefix)group.com.internxt.snacks
CADisableMinimumFrameDurationOnPhone
CFBundleDevelopmentRegion
diff --git a/ios/Internxt/Internxt.entitlements b/ios/Internxt/Internxt.entitlements
index 7ab3cacb8..c8c92a781 100644
--- a/ios/Internxt/Internxt.entitlements
+++ b/ios/Internxt/Internxt.entitlements
@@ -24,5 +24,9 @@
group.com.internxt.snacks
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)group.com.internxt.snacks
+
\ No newline at end of file
diff --git a/ios/InternxtShareExtension/Info.plist b/ios/InternxtShareExtension/Info.plist
index 40835fcdd..3d5f291ca 100644
--- a/ios/InternxtShareExtension/Info.plist
+++ b/ios/InternxtShareExtension/Info.plist
@@ -6,6 +6,8 @@
group.com.internxt.snacks
AppGroupIdentifier
group.com.internxt.snacks
+ SharedKeychainGroup
+ $(AppIdentifierPrefix)group.com.internxt.snacks
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
diff --git a/ios/InternxtShareExtension/InternxtShareExtension.entitlements b/ios/InternxtShareExtension/InternxtShareExtension.entitlements
index 31b97d4d2..4ede752b5 100644
--- a/ios/InternxtShareExtension/InternxtShareExtension.entitlements
+++ b/ios/InternxtShareExtension/InternxtShareExtension.entitlements
@@ -6,5 +6,9 @@
group.com.internxt.snacks
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)group.com.internxt.snacks
+
\ No newline at end of file
diff --git a/ios/InternxtShareExtension/ShareExtensionViewController.swift b/ios/InternxtShareExtension/ShareExtensionViewController.swift
index cd0a5f803..4958ed645 100644
--- a/ios/InternxtShareExtension/ShareExtensionViewController.swift
+++ b/ios/InternxtShareExtension/ShareExtensionViewController.swift
@@ -1,22 +1,26 @@
+// ─── From expo-share-extension library ───────────────────────────────────────
import UIKit
import React
import React_RCTAppDelegate
import ReactAppDependencyProvider
import AVFoundation
import UniformTypeIdentifiers
-// if react native firebase is installed, we import and configure it
+// ─── Internxt additions ───────────────────────────────────────────────────────
+import Security
+
#if canImport(FirebaseCore)
import FirebaseCore
#endif
#if canImport(FirebaseAuth)
import FirebaseAuth
#endif
+// ─────────────────────────────────────────────────────────────────────────────
class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate {
override func sourceURL(for _: RCTBridge) -> URL? {
self.bundleURL()
}
-
+
override func bundleURL() -> URL? {
#if DEBUG
let settings = RCTBundleURLProvider.sharedSettings()
@@ -46,103 +50,96 @@ class ShareExtensionViewController: UIViewController {
private var isCleanedUp = false
deinit {
- print("🧹 ShareExtensionViewController deinit")
cleanupAfterClose()
}
-
+
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
- // Start cleanup earlier to ensure proper surface teardown
if isBeingDismissed {
cleanupAfterClose()
}
}
-
+
override func viewDidLoad() {
super.viewDidLoad()
setupLoadingIndicator()
isCleanedUp = false
-
- // Set the contentScaleFactor for the main view of this view controller
self.view.contentScaleFactor = UIScreen.main.scale
-
+
#if canImport(FirebaseCore)
if Bundle.main.object(forInfoDictionaryKey: "WithFirebase") as? Bool ?? false {
FirebaseApp.configure()
}
#endif
-
+
loadReactNativeContent()
setupNotificationCenterObserver()
}
-
+
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
- // we need to clean up when the view is closed via a swipe
cleanupAfterClose()
}
-
+
func close() {
self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
- // we need to clean up when the view is closed via the close() method in react native
cleanupAfterClose()
}
-
+
private func loadReactNativeContent() {
getShareData { [weak self] sharedData in
- guard let self = self else {
- print("❌ Self was deallocated")
- return
- }
-
+ guard let self = self else { return }
+
+ // ── From expo-share-extension library ──────────────────────────────────
reactNativeFactoryDelegate = ReactNativeDelegate()
reactNativeFactoryDelegate!.dependencyProvider = RCTAppDependencyProvider()
reactNativeFactory = RCTReactNativeFactory(delegate: reactNativeFactoryDelegate!)
-
+
var initialProps = sharedData ?? [:]
-
- // Capture current view's properties before replacing it
+ // ── Internxt: inject auth state from UserDefaults and Keychain ──────────
+ if let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String,
+ let defaults = UserDefaults(suiteName: appGroup) {
+ initialProps["isAuthenticated"] = defaults.bool(forKey: "isAuthenticated")
+ initialProps["userEmail"] = defaults.string(forKey: "userEmail")
+ }
+
+ if let sharedGroup = Bundle.main.object(forInfoDictionaryKey: "SharedKeychainGroup") as? String {
+ initialProps["photosToken"] = readFromSharedKeychain(key: "shared_photosToken", accessGroup: sharedGroup)
+ if let raw = readFromSharedKeychain(key: "shared_mnemonic", accessGroup: sharedGroup) {
+ // Strip JSON string encoding added by expo-secure-store: '"words"' → 'words'
+ initialProps["mnemonic"] = (raw.hasPrefix("\"") && raw.hasSuffix("\""))
+ ? String(raw.dropFirst().dropLast())
+ : raw
+ }
+ }
+ // ── From expo-share-extension library ──────────────────────────────────
let currentBounds = self.view.bounds
- let currentScale = UIScreen.main.scale
-
- // Log the scale of the parent view
- print("[ShareExtension] self.view.contentScaleFactor before adding subview: \(self.view.contentScaleFactor)")
- print("[ShareExtension] UIScreen.main.scale: \(currentScale)")
-
- // Add screen metrics to initial properties for React Native
- // These can be used by the JS side to understand its container size and scale
initialProps["initialViewWidth"] = currentBounds.width
initialProps["initialViewHeight"] = currentBounds.height
- initialProps["pixelRatio"] = currentScale
- // It's also good practice to pass the font scale for accessibility
- // Default body size on iOS is 17pt, used as a reference for calculating fontScale.
+ initialProps["pixelRatio"] = UIScreen.main.scale
initialProps["fontScale"] = UIFont.preferredFont(forTextStyle: .body).pointSize / 17.0
-
- // Create the React Native root view
+
let reactNativeRootView = reactNativeFactory!.rootViewFactory.view(
withModuleName: "shareExtension",
initialProperties: initialProps
)
-
+
let backgroundFromInfoPlist = Bundle.main.object(forInfoDictionaryKey: "ShareExtensionBackgroundColor") as? [String: CGFloat]
let heightFromInfoPlist = Bundle.main.object(forInfoDictionaryKey: "ShareExtensionHeight") as? CGFloat
-
+
configureRootView(reactNativeRootView, withBackgroundColorDict: backgroundFromInfoPlist, withHeight: heightFromInfoPlist)
view.addSubview(reactNativeRootView)
- // Hide loading indicator once React content is ready
self.loadingIndicator.stopAnimating()
self.loadingIndicator.removeFromSuperview()
+ // ───────────────────────────────────────────────────────────────────────
}
}
-
+
private func configureRootView(_ rootView: UIView, withBackgroundColorDict dict: [String: CGFloat]?, withHeight: CGFloat?) {
rootView.backgroundColor = backgroundColor(from: dict)
- // Get the screen bounds
let screenBounds = UIScreen.main.bounds
-
- // Calculate proper frame
let frame: CGRect
if let withHeight = withHeight {
rootView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
@@ -158,7 +155,7 @@ class ShareExtensionViewController: UIViewController {
}
rootView.frame = frame
}
-
+
private func setupLoadingIndicator() {
view.addSubview(loadingIndicator)
loadingIndicator.translatesAutoresizingMaskIntoConstraints = false
@@ -168,19 +165,18 @@ class ShareExtensionViewController: UIViewController {
])
loadingIndicator.startAnimating()
}
-
+
private func openHostApp(path: String?) {
guard let scheme = Bundle.main.object(forInfoDictionaryKey: "HostAppScheme") as? String else { return }
var urlComponents = URLComponents()
urlComponents.scheme = scheme
urlComponents.host = ""
-
+
if let path = path {
let pathComponents = path.split(separator: "?", maxSplits: 1)
let pathWithoutQuery = String(pathComponents[0])
let queryString = pathComponents.count > 1 ? String(pathComponents[1]) : nil
-
- // Parse and set query items
+
if let queryString = queryString {
let queryItems = queryString.split(separator: "&").map { queryParam -> URLQueryItem in
let paramComponents = queryParam.split(separator: "=", maxSplits: 1)
@@ -190,7 +186,7 @@ class ShareExtensionViewController: UIViewController {
}
urlComponents.queryItems = queryItems
}
-
+
var baseComponents = URLComponents()
baseComponents.scheme = scheme
baseComponents.host = ""
@@ -199,12 +195,12 @@ class ShareExtensionViewController: UIViewController {
urlComponents.path = baseURL.appendingPathComponent(strippedPath).path
}
}
-
+
guard let url = urlComponents.url else { return }
openURL(url)
self.close()
}
-
+
@objc @discardableResult private func openURL(_ url: URL) -> Bool {
// Method 1: Try responder chain to find UIApplication
var responder: UIResponder? = self
@@ -216,7 +212,7 @@ class ShareExtensionViewController: UIViewController {
responder = responder?.next
}
- // Method 2: Try selector-based approach as fallback
+ // Method 2: Selector-based fallback
let selector = NSSelectorFromString("openURL:")
var responder2: UIResponder? = self
while responder2 != nil {
@@ -229,14 +225,14 @@ class ShareExtensionViewController: UIViewController {
return false
}
-
+
private func setupNotificationCenterObserver() {
NotificationCenter.default.addObserver(forName: NSNotification.Name("close"), object: nil, queue: nil) { [weak self] _ in
DispatchQueue.main.async {
self?.close()
}
}
-
+
NotificationCenter.default.addObserver(forName: NSNotification.Name("openHostApp"), object: nil, queue: nil) { [weak self] notification in
DispatchQueue.main.async {
if let userInfo = notification.userInfo, let path = userInfo["path"] as? String {
@@ -245,26 +241,43 @@ class ShareExtensionViewController: UIViewController {
}
}
}
-
+
private func cleanupAfterClose() {
if isCleanedUp { return }
isCleanedUp = true
-
+
NotificationCenter.default.removeObserver(self)
-
- // Remove React Native view and deallocate resources
+
view.subviews.forEach { subview in
- if subview is RCTRootView {
- subview.removeFromSuperview()
- }
+ if subview is RCTRootView {
+ subview.removeFromSuperview()
+ }
}
-
+
reactNativeFactory = nil
reactNativeFactoryDelegate = nil
-
- print("🧹 ShareExtensionViewController cleaned up")
}
-
+
+ // ── Internxt: read a value from the shared Keychain access group ────────────
+ private func readFromSharedKeychain(key: String, accessGroup: String) -> String? {
+ var result: AnyObject?
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: "app:no-auth",
+ kSecAttrGeneric as String: Data(key.utf8),
+ kSecAttrAccount as String: Data(key.utf8),
+ kSecAttrAccessGroup as String: accessGroup,
+ kSecMatchLimit as String: kSecMatchLimitOne,
+ kSecReturnData as String: true,
+ ]
+ guard SecItemCopyMatching(query as CFDictionary, &result) == errSecSuccess,
+ let data = result as? Data,
+ let value = String(data: data, encoding: .utf8) else { return nil }
+ return value
+ }
+ // ─────────────────────────────────────────────────────────────────────────
+
+ // ── From expo-share-extension library ─────────────────────────────────────
private func backgroundColor(from dict: [String: CGFloat]?) -> UIColor {
guard let dict = dict else { return .systemBackground }
let red = dict["red"] ?? 255.0
@@ -273,19 +286,21 @@ class ShareExtensionViewController: UIViewController {
let alpha = dict["alpha"] ?? 1
return UIColor(red: red / 255.0, green: green / 255.0, blue: blue / 255.0, alpha: alpha)
}
-
+
+ // ─── From expo-share-extension library ───────────────────────────────────────
+ // Reads the NSExtensionItem attachments from the share context and classifies
+ // each one by UTType into: images / videos / files / url / text.
+ // The result is passed as initialProperties to the React Native root view.
private func getShareData(completion: @escaping ([String: Any]?) -> Void) {
guard let extensionItems = extensionContext?.inputItems as? [NSExtensionItem] else {
completion(nil)
return
}
-
+
var sharedItems: [String: Any] = [:]
-
let group = DispatchGroup()
-
let fileManager = FileManager.default
-
+
for item in extensionItems {
for provider in item.attachments ?? [] {
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
@@ -298,29 +313,28 @@ class ShareExtensionViewController: UIViewController {
let fileExtension = sharedURL.pathExtension.lowercased()
let imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "tiff", "tif", "heic", "heif", "webp"]
var isImage = imageExtensions.contains(fileExtension)
-
+
if !isImage, let resourceValues = try? sharedURL.resourceValues(forKeys: [.typeIdentifierKey]),
let typeIdentifier = resourceValues.typeIdentifier {
isImage = UTType(typeIdentifier)?.conforms(to: .image) ?? false
}
-
+
guard let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String else {
print("Could not find AppGroup in info.plist")
group.leave()
return
}
-
+
guard let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
print("Could not set up file manager container URL for app group")
group.leave()
return
}
-
+
let tempFilePath = sharedURL.path
let fileName = sharedURL.lastPathComponent
-
let sharedDataUrl = containerUrl.appendingPathComponent("sharedData")
-
+
if !fileManager.fileExists(atPath: sharedDataUrl.path) {
do {
try fileManager.createDirectory(at: sharedDataUrl, withIntermediateDirectories: true)
@@ -328,9 +342,9 @@ class ShareExtensionViewController: UIViewController {
print("Failed to create sharedData directory: \(error)")
}
}
-
+
let persistentURL = sharedDataUrl.appendingPathComponent(fileName)
-
+
do {
try fileManager.copyItem(atPath: tempFilePath, toPath: persistentURL.path)
let key = isImage ? "images" : "files"
@@ -383,29 +397,28 @@ class ShareExtensionViewController: UIViewController {
group.enter()
provider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (imageItem, error) in
DispatchQueue.main.async {
-
+
// Ensure the array exists
if sharedItems["images"] == nil {
sharedItems["images"] = [String]()
}
-
+
guard let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String else {
print("Could not find AppGroup in info.plist")
return
}
-
+
guard let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
print("Could not set up file manager container URL for app group")
return
}
-
+
if let imageUri = imageItem as? NSURL {
if let tempFilePath = imageUri.path {
let fileExtension = imageUri.pathExtension ?? "jpg"
let fileName = UUID().uuidString + "." + fileExtension
-
let sharedDataUrl = containerUrl.appendingPathComponent("sharedData")
-
+
if !fileManager.fileExists(atPath: sharedDataUrl.path) {
do {
try fileManager.createDirectory(at: sharedDataUrl, withIntermediateDirectories: true)
@@ -413,14 +426,14 @@ class ShareExtensionViewController: UIViewController {
print("Failed to create sharedData directory: \(error)")
}
}
-
+
let persistentURL = sharedDataUrl.appendingPathComponent(fileName)
-
+
do {
try fileManager.copyItem(atPath: tempFilePath, toPath: persistentURL.path)
- if var videoArray = sharedItems["images"] as? [String] {
- videoArray.append(persistentURL.absoluteString)
- sharedItems["images"] = videoArray
+ if var imageArray = sharedItems["images"] as? [String] {
+ imageArray.append(persistentURL.absoluteString)
+ sharedItems["images"] = imageArray
}
} catch {
print("Failed to copy image: \(error)")
@@ -430,9 +443,8 @@ class ShareExtensionViewController: UIViewController {
// Handle UIImage if needed (e.g., save to disk and get the file path)
if let imageData = image.jpegData(compressionQuality: 1.0) {
let fileName = UUID().uuidString + ".jpg"
-
let sharedDataUrl = containerUrl.appendingPathComponent("sharedData")
-
+
if !fileManager.fileExists(atPath: sharedDataUrl.path) {
do {
try fileManager.createDirectory(at: sharedDataUrl, withIntermediateDirectories: true)
@@ -440,9 +452,9 @@ class ShareExtensionViewController: UIViewController {
print("Failed to create sharedData directory: \(error)")
}
}
-
+
let persistentURL = sharedDataUrl.appendingPathComponent(fileName)
-
+
do {
try imageData.write(to: persistentURL)
if var imageArray = sharedItems["images"] as? [String] {
@@ -489,30 +501,29 @@ class ShareExtensionViewController: UIViewController {
provider.loadItem(forTypeIdentifier: UTType.movie.identifier, options: nil) { (videoItem, error) in
DispatchQueue.main.async {
print("videoItem type: \(type(of: videoItem))")
-
+
// Ensure the array exists
if sharedItems["videos"] == nil {
sharedItems["videos"] = [String]()
}
-
+
guard let appGroup = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String else {
print("Could not find AppGroup in info.plist")
return
}
-
+
guard let containerUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
print("Could not set up file manager container URL for app group")
return
}
-
+
// Check if videoItem is NSURL
if let videoUri = videoItem as? NSURL {
if let tempFilePath = videoUri.path {
let fileExtension = videoUri.pathExtension ?? "mov"
let fileName = UUID().uuidString + "." + fileExtension
-
let sharedDataUrl = containerUrl.appendingPathComponent("sharedData")
-
+
if !fileManager.fileExists(atPath: sharedDataUrl.path) {
do {
try fileManager.createDirectory(at: sharedDataUrl, withIntermediateDirectories: true)
@@ -520,9 +531,9 @@ class ShareExtensionViewController: UIViewController {
print("Failed to create sharedData directory: \(error)")
}
}
-
+
let persistentURL = sharedDataUrl.appendingPathComponent(fileName)
-
+
do {
try fileManager.copyItem(atPath: tempFilePath, toPath: persistentURL.path)
if var videoArray = sharedItems["videos"] as? [String] {
@@ -538,9 +549,8 @@ class ShareExtensionViewController: UIViewController {
else if let videoData = videoItem as? NSData {
let fileExtension = "mov" // Using mov as default type extension
let fileName = UUID().uuidString + "." + fileExtension
-
let sharedDataUrl = containerUrl.appendingPathComponent("sharedData")
-
+
if !fileManager.fileExists(atPath: sharedDataUrl.path) {
do {
try fileManager.createDirectory(at: sharedDataUrl, withIntermediateDirectories: true)
@@ -548,9 +558,9 @@ class ShareExtensionViewController: UIViewController {
print("Failed to create sharedData directory: \(error)")
}
}
-
+
let persistentURL = sharedDataUrl.appendingPathComponent(fileName)
-
+
do {
try videoData.write(to: persistentURL)
if var videoArray = sharedItems["videos"] as? [String] {
@@ -564,12 +574,12 @@ class ShareExtensionViewController: UIViewController {
// Check if videoItem is AVAsset
else if let asset = videoItem as? AVAsset {
let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetPassthrough)
-
+
let fileExtension = "mov" // Using mov as default type extension
let fileName = UUID().uuidString + "." + fileExtension
-
+
let sharedDataUrl = containerUrl.appendingPathComponent("sharedData")
-
+
if !fileManager.fileExists(atPath: sharedDataUrl.path) {
do {
try fileManager.createDirectory(at: sharedDataUrl, withIntermediateDirectories: true)
@@ -577,9 +587,9 @@ class ShareExtensionViewController: UIViewController {
print("Failed to create sharedData directory: \(error)")
}
}
-
+
let persistentURL = sharedDataUrl.appendingPathComponent(fileName)
-
+
exportSession?.outputURL = persistentURL
exportSession?.outputFileType = .mov
func onExportComplete() {
@@ -605,7 +615,7 @@ class ShareExtensionViewController: UIViewController {
}
}
}
-
+
group.notify(queue: .main) {
completion(sharedItems.isEmpty ? nil : sharedItems)
}
diff --git a/src/shareExtension/AndroidShareScreen.tsx b/src/shareExtension/AndroidShareScreen.tsx
index 523b6bff4..117c30863 100644
--- a/src/shareExtension/AndroidShareScreen.tsx
+++ b/src/shareExtension/AndroidShareScreen.tsx
@@ -1,73 +1,103 @@
-import { ScrollView, StyleSheet, Text, View } from 'react-native';
+import { useEffect, useState } from 'react';
+import { ActivityIndicator, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+import { useTailwind } from 'tailwind-rn';
+import strings from '../../assets/lang/strings';
+import asyncStorageService from '../services/AsyncStorageService';
+import { AsyncStorageKey } from '../types';
import { RootStackScreenProps } from '../types/navigation';
+import { useShareAuth } from './hooks/useShareAuth.android';
+import { NotSignedInScreen } from './screens/NotSignedInScreen';
-const AndroidShareScreen = ({ route }: RootStackScreenProps<'AndroidShare'>) => {
+interface DebugInfo {
+ userEmail: string | null;
+ photosToken: string | null;
+ mnemonic: string | null;
+}
+
+const AndroidShareScreen = ({ navigation, route }: RootStackScreenProps<'AndroidShare'>) => {
+ const tailwind = useTailwind();
+ const authStatus = useShareAuth();
+ const [debug, setDebug] = useState({ userEmail: null, photosToken: null, mnemonic: null });
+
+ useEffect(() => {
+ if (authStatus !== 'authenticated') return;
+ Promise.all([asyncStorageService.getUser(), asyncStorageService.getItem(AsyncStorageKey.PhotosToken)]).then(
+ ([user, photosToken]) => {
+ setDebug({
+ userEmail: user?.email ?? null,
+ photosToken,
+ mnemonic: user?.mnemonic ?? null,
+ });
+ },
+ );
+ }, [authStatus]);
+
+ if (authStatus === 'loading') {
+ return (
+
+
+
+ );
+ }
+
+ if (authStatus === 'unauthenticated') {
+ return navigation.goBack()} onOpenLogin={() => navigation.navigate('SignIn')} />;
+ }
+
+ const translations = strings.screens.ShareExtension;
const files = route.params?.files ?? [];
return (
-
- Save to Internxt
-
- {files.length} {files.length === 1 ? 'file' : 'files'}
-
+
+
+ navigation.goBack()} style={tailwind('w-8 h-8 items-center justify-center')}>
+ ✕
+
+ {translations.title}
+
+
- {files.map((file) => (
-
-
-
+ {debug.userEmail ? (
+
+ Signed in as
+ {debug.userEmail}
+
+ ) : null}
+ {debug.photosToken ? (
+
+ Signed new token
+ {debug.photosToken}
- ))}
-
+ ) : null}
+ {debug.mnemonic ? (
+
+ Signed in with mnemonic
+ {debug.mnemonic}
+
+ ) : null}
+
+
+ {files.map((file) => {
+ const name = file.fileName ?? file.uri.split('/').pop() ?? file.uri;
+ return (
+
+
+ {name}
+
+
+ );
+ })}
+
+
);
};
-const Row = ({ label, value }: { label: string; value: string }) => (
-
- {label}
-
- {value}
-
-
-);
-
const styles = StyleSheet.create({
- container: {
- padding: 24,
- paddingTop: 60,
- },
- title: {
- fontSize: 20,
- fontWeight: '700',
- color: '#000000',
- marginBottom: 4,
- },
- count: {
- fontSize: 14,
- color: '#6b7280',
- marginBottom: 20,
- },
- card: {
- backgroundColor: '#f9fafb',
- borderRadius: 10,
- padding: 14,
- marginBottom: 12,
- gap: 6,
- },
- row: {
- flexDirection: 'row',
- gap: 8,
- },
- label: {
- fontSize: 12,
- fontWeight: '600',
- color: '#6b7280',
- width: 44,
- paddingTop: 1,
- },
- value: {
- fontSize: 13,
- color: '#111827',
- flex: 1,
+ semibold: { fontWeight: '600' },
+ blueSection: {
+ backgroundColor: 'rgba(0,102,255,0.05)',
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(0,102,255,0.2)',
},
});
diff --git a/src/shareExtension/ShareExtensionApp.tsx b/src/shareExtension/ShareExtensionApp.tsx
index 32e2a2de5..f8c6c0c74 100644
--- a/src/shareExtension/ShareExtensionApp.tsx
+++ b/src/shareExtension/ShareExtensionApp.tsx
@@ -1,122 +1,147 @@
-import { File } from 'expo-file-system';
-import { close, type InitialProps } from 'expo-share-extension';
-import prettysize from 'prettysize';
-import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
+import { close, openHostApp } from 'expo-share-extension';
+import { ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
+import strings from '../../assets/lang/strings';
+import { NotSignedInScreen } from './screens/NotSignedInScreen';
-interface FileRow {
- path: string;
- name: string;
- size: string | null;
+interface ShareExtensionProps {
+ isAuthenticated?: boolean;
+ userEmail?: string;
+ photosToken?: string;
+ mnemonic?: string;
+ files?: string[];
+ images?: string[];
+ videos?: string[];
+ url?: string;
+ text?: string;
}
-function buildRow(path: string): FileRow {
- const name = path.split('/').pop() ?? path;
- try {
- const file = new File(path);
- const size = file.exists && file.size > 0 ? prettysize(file.size) : null;
- return { path, name, size };
- } catch {
- return { path, name, size: null };
+const ShareExtensionApp = (props: ShareExtensionProps) => {
+ const translations = strings.screens.ShareExtension;
+
+ if (!props.isAuthenticated) {
+ return openHostApp('sign-in')} />;
}
-}
-const ShareExtensionApp = (props: InitialProps) => {
- const rows = [...(props.files ?? []), ...(props.images ?? []), ...(props.videos ?? [])].map(buildRow);
+ const allFiles = [...(props.files ?? []), ...(props.images ?? []), ...(props.videos ?? [])];
return (
-
-
- ✕
-
- Save to Internxt
-
- {rows.length} {rows.length === 1 ? 'file' : 'files'}
-
-
- {rows.map((file) => (
-
-
-
- {file.size &&
}
+
+
+
+ ✕
+
+ {translations.title}
+
+
+ {props.userEmail ? (
+
+ Signed in as
+ {props.userEmail}
- ))}
-
- {props.text && (
-
-
+ ) : null}
+ {props.photosToken ? (
+
+ Signed new token
+ {props.photosToken}
- )}
- {props.url && (
-
-
+ ) : null}
+ {props.mnemonic ? (
+
+ Signed in with mnemonic
+ {props.mnemonic}
- )}
-
+ ) : null}
+
+ {allFiles.map((filePath) => {
+ const name = filePath.split('/').pop() ?? filePath;
+ return (
+
+
+ {name}
+
+
+ );
+ })}
+ {props.url ? (
+
+
+ {props.url}
+
+
+ ) : null}
+ {props.text ? (
+
+
+ {props.text}
+
+
+ ) : null}
+
+
);
};
-const Row = ({ label, value }: { label: string; value: string }) => (
-
- {label}
-
- {value}
-
-
-);
-
const styles = StyleSheet.create({
container: {
- padding: 24,
- paddingTop: 60,
+ flex: 1,
+ backgroundColor: '#ffffff',
},
- closeButton: {
- position: 'absolute',
- top: 16,
- right: 16,
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 16,
+ paddingTop: 16,
+ paddingBottom: 12,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: '#e5e7eb',
+ },
+ closeBtn: {
width: 32,
height: 32,
- borderRadius: 16,
- backgroundColor: '#e5e7eb',
alignItems: 'center',
justifyContent: 'center',
},
closeText: {
- fontSize: 14,
- color: '#374151',
+ fontSize: 18,
+ color: '#6b7280',
},
title: {
- fontSize: 20,
- fontWeight: '700',
- color: '#000000',
- marginBottom: 4,
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#1a1a1a',
},
- count: {
- fontSize: 14,
- color: '#6b7280',
- marginBottom: 20,
+ content: {
+ padding: 16,
+ gap: 8,
},
- card: {
+ fileRow: {
backgroundColor: '#f9fafb',
- borderRadius: 10,
- padding: 14,
- marginBottom: 12,
- gap: 6,
+ borderRadius: 8,
+ padding: 12,
+ borderWidth: StyleSheet.hairlineWidth,
+ borderColor: '#e5e7eb',
},
- row: {
- flexDirection: 'row',
- gap: 8,
+ fileName: {
+ fontSize: 14,
+ color: '#374151',
},
- label: {
- fontSize: 12,
- fontWeight: '600',
+ sessionBanner: {
+ backgroundColor: '#f0f7ff',
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: '#bfdbfe',
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ },
+ sessionLabel: {
+ fontSize: 11,
color: '#6b7280',
- width: 44,
- paddingTop: 1,
+ marginBottom: 2,
},
- value: {
+ sessionEmail: {
fontSize: 13,
- color: '#111827',
- flex: 1,
+ fontWeight: '600',
+ color: '#0066FF',
},
});
diff --git a/src/shareExtension/hooks/useShareAuth.android.ts b/src/shareExtension/hooks/useShareAuth.android.ts
new file mode 100644
index 000000000..faffed471
--- /dev/null
+++ b/src/shareExtension/hooks/useShareAuth.android.ts
@@ -0,0 +1,18 @@
+import { useEffect, useState } from 'react';
+import asyncStorageService from '../../services/AsyncStorageService';
+import { AsyncStorageKey } from '../../types';
+
+export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';
+
+export const useShareAuth = (): AuthStatus => {
+ const [status, setStatus] = useState('loading');
+
+ useEffect(() => {
+ asyncStorageService
+ .getItem(AsyncStorageKey.PhotosToken)
+ .then((token) => setStatus(token ? 'authenticated' : 'unauthenticated'))
+ .catch(() => setStatus('unauthenticated'));
+ }, []);
+
+ return status;
+};
diff --git a/src/shareExtension/screens/NotSignedInScreen.tsx b/src/shareExtension/screens/NotSignedInScreen.tsx
new file mode 100644
index 000000000..008bec8fa
--- /dev/null
+++ b/src/shareExtension/screens/NotSignedInScreen.tsx
@@ -0,0 +1,136 @@
+import { Platform, Pressable, StyleSheet, Text, View } from 'react-native';
+import Svg, { Path } from 'react-native-svg';
+import strings from '../../../assets/lang/strings';
+
+interface NotSignedInScreenProps {
+ onClose: () => void;
+ onOpenLogin: () => void;
+}
+
+function LoginIcon() {
+ return (
+
+ );
+}
+
+export function NotSignedInScreen({ onClose, onOpenLogin }: NotSignedInScreenProps) {
+ const translations = strings.screens.ShareExtension;
+
+ return (
+
+
+
+
+
+ ✕
+
+ {translations.title}
+
+
+
+
+
+ {translations.notSignedIn.title}
+ {translations.notSignedIn.subtitle}
+
+ {translations.notSignedIn.openLogin}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#ffffff',
+ },
+ handle: {
+ width: 36,
+ height: 4,
+ borderRadius: 2,
+ backgroundColor: '#d1d5db',
+ alignSelf: 'center',
+ marginTop: 8,
+ marginBottom: 4,
+ },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingHorizontal: 16,
+ paddingTop: 12,
+ paddingBottom: 12,
+ borderBottomWidth: StyleSheet.hairlineWidth,
+ borderBottomColor: '#e5e7eb',
+ },
+ closeButton: {
+ width: 32,
+ height: 32,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ closeText: {
+ fontSize: 18,
+ color: '#6b7280',
+ },
+ headerTitle: {
+ flex: 1,
+ textAlign: 'center',
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#111827',
+ },
+ headerSpacer: {
+ width: 32,
+ },
+ body: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingHorizontal: 24,
+ paddingBottom: 36,
+ gap: 12,
+ },
+ title: {
+ fontFamily: Platform.select({ android: 'InstrumentSans-SemiBold' }),
+ fontWeight: '600',
+ fontSize: Platform.select({ ios: 36, android: 30 }),
+ lineHeight: Platform.select({ ios: 44, android: 36 }),
+ color: '#1C1C1C',
+ textAlign: 'center',
+ marginTop: 8,
+ },
+ subtitle: {
+ fontFamily: Platform.select({ android: 'InstrumentSans-Regular' }),
+ fontWeight: '400',
+ fontSize: Platform.select({ ios: 20, android: 18 }),
+ lineHeight: Platform.select({ ios: 24, android: 22 }),
+ color: '#737373',
+ textAlign: 'center',
+ },
+ loginButton: {
+ backgroundColor: '#0066FF',
+ borderRadius: 12,
+ paddingVertical: 16,
+ alignSelf: 'stretch',
+ alignItems: 'center',
+ justifyContent: 'center',
+ marginTop: 12,
+ },
+ loginButtonText: {
+ fontFamily: Platform.select({ android: 'InstrumentSans-SemiBold' }),
+ fontWeight: '600',
+ fontSize: 16,
+ lineHeight: 20,
+ textAlign: 'center',
+ color: '#ffffff',
+ },
+});
diff --git a/src/shareExtension/useAndroidShareIntent.ts b/src/shareExtension/useAndroidShareIntent.ts
index 423671878..70e6ee049 100644
--- a/src/shareExtension/useAndroidShareIntent.ts
+++ b/src/shareExtension/useAndroidShareIntent.ts
@@ -24,10 +24,6 @@ export function useAndroidShareIntent(
useEffect(() => {
if (!pendingFiles?.length || isLoggedIn == null) return;
- if (!isLoggedIn) {
- setPendingFiles(null);
- return;
- }
navigationContainerRef?.navigate('AndroidShare', { files: pendingFiles });
setPendingFiles(null);
}, [pendingFiles, isLoggedIn, navigationContainerRef]);