Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,7 @@ const appConfig: ExpoConfig & { extra: AppEnv & { NODE_ENV: AppStage; RELEASE_ID
'react-native-create-thumbnail',
'jail-monkey',
],
backgroundColor: { red: 255, green: 255, blue: 255, alpha: 1 },
height: 700,
backgroundColor: { red: 0, green: 0, blue: 0, alpha: 0 },
},
],
],
Expand Down
14 changes: 14 additions & 0 deletions assets/icons/stacked-files.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 26 additions & 0 deletions assets/lang/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,19 @@ const translations = {
subtitle: 'Log in to upload files securely',
openLogin: 'Open Internxt login',
},
multipleFormats: 'Multiple formats',
fileNameFallback: 'File',
rename: 'Rename',
itemsSelected: '{0} items selected',
searchPlaceholder: 'Search in Drive',
rootFolderName: 'Drive',
noResults: 'No results found',
emptyFolder: 'This folder is empty',
sortByName: 'Name ↑',
folderNameLabel: 'Name',
folderNamePlaceholder: 'Folder name',
folderNameEmpty: 'Folder name cannot be empty',
folderCreateError: 'Failed to create folder. Please try again.',
},
},
buttons: {
Expand Down Expand Up @@ -1141,6 +1154,19 @@ const translations = {
subtitle: 'Inicia sesión para subir archivos de forma segura',
openLogin: 'Abrir login de Internxt',
},
multipleFormats: 'Múltiples formatos',
fileNameFallback: 'Archivo',
rename: 'Renombrar',
itemsSelected: '{0} elementos seleccionados',
searchPlaceholder: 'Buscar en Drive',
rootFolderName: 'Drive',
noResults: 'Sin resultados',
emptyFolder: 'Esta carpeta está vacía',
sortByName: 'Nombre ↑',
folderNameLabel: 'Nombre',
folderNamePlaceholder: 'Nombre de carpeta',
folderNameEmpty: 'El nombre no puede estar vacío',
folderCreateError: 'Error al crear la carpeta. Inténtalo de nuevo.',
},
},
buttons: {
Expand Down
10 changes: 8 additions & 2 deletions index.share.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { createElement } from 'react';
import { AppRegistry } from 'react-native';
import ShareExtensionApp from './src/shareExtension/ShareExtensionApp';
import { TailwindProvider } from 'tailwind-rn';
import ShareExtensionView from './src/shareExtension/ShareExtensionView.ios';
import utilities from './src/styles/tailwind.json';

AppRegistry.registerComponent('shareExtension', () => ShareExtensionApp);
AppRegistry.registerComponent(
'shareExtension',
() => (props) => createElement(TailwindProvider, { utilities }, createElement(ShareExtensionView, props)),
);
31 changes: 9 additions & 22 deletions ios/Internxt/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,37 +44,24 @@ public class AppDelegate: ExpoAppDelegate {
// 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
guard 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")
copyToSharedKeychain(privateKey: "photosToken", sharedKey: "shared_photosToken", accessGroup: sharedGroup)
copyToSharedKeychain(privateKey: "xUser_mnemonic", sharedKey: "shared_mnemonic", accessGroup: sharedGroup)
copyToSharedKeychain(privateKey: "xUser_rootFolderId", sharedKey: "shared_rootFolderId", accessGroup: sharedGroup)
copyToSharedKeychain(privateKey: "xUser_bucket", sharedKey: "shared_bucket", accessGroup: sharedGroup)
} else {
deleteFromSharedKeychain(key: "shared_photosToken", accessGroup: sharedGroup)
deleteFromSharedKeychain(key: "shared_mnemonic", accessGroup: sharedGroup)
defaults.removeObject(forKey: "userEmail")
deleteFromSharedKeychain(key: "shared_photosToken", accessGroup: sharedGroup)
deleteFromSharedKeychain(key: "shared_mnemonic", accessGroup: sharedGroup)
deleteFromSharedKeychain(key: "shared_rootFolderId", accessGroup: sharedGroup)
deleteFromSharedKeychain(key: "shared_bucket", accessGroup: sharedGroup)
}
}

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
}
Expand Down
29 changes: 15 additions & 14 deletions ios/InternxtShareExtension/ShareExtensionViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class ShareExtensionViewController: UIViewController {
super.viewDidLoad()
setupLoadingIndicator()
isCleanedUp = false
self.view.backgroundColor = .clear
self.view.contentScaleFactor = UIScreen.main.scale

#if canImport(FirebaseCore)
Expand Down Expand Up @@ -96,21 +97,12 @@ class ShareExtensionViewController: UIViewController {
reactNativeFactory = RCTReactNativeFactory(delegate: reactNativeFactoryDelegate!)

var initialProps = sharedData ?? [:]
// ── 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")
}

// ── Internxt: inject auth state from Keychain ───────────────────────────
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
}
initialProps["photosToken"] = readFromSharedKeychain(key: "shared_photosToken", accessGroup: sharedGroup)
initialProps["mnemonic"] = readFromSharedKeychainStripped(key: "shared_mnemonic", accessGroup: sharedGroup)
initialProps["rootFolderId"] = readFromSharedKeychainStripped(key: "shared_rootFolderId", accessGroup: sharedGroup)
initialProps["bucket"] = readFromSharedKeychainStripped(key: "shared_bucket", accessGroup: sharedGroup)
}
// ── From expo-share-extension library ──────────────────────────────────
let currentBounds = self.view.bounds
Expand Down Expand Up @@ -275,6 +267,15 @@ class ShareExtensionViewController: UIViewController {
let value = String(data: data, encoding: .utf8) else { return nil }
return value
}

/// Reads a shared-keychain entry and strips surrounding JSON quotes if present.
private func readFromSharedKeychainStripped(key: String, accessGroup: String) -> String? {
guard let raw = readFromSharedKeychain(key: key, accessGroup: accessGroup) else { return nil }
if raw.hasPrefix("\"") && raw.hasSuffix("\"") {
return String(raw.dropFirst().dropLast())
}
return raw
}
// ─────────────────────────────────────────────────────────────────────────

// ── From expo-share-extension library ─────────────────────────────────────
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"axios": "^1.13.5",
"base-64": "^1.0.0",
"crypto-js": "=3.1.9-1",
"dayjs": "^1.11.19",
"events": "^3.3.0",
"expo": "^54",
"expo-asset": "~12.0.12",
Expand Down
1 change: 1 addition & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ function AppContent(): JSX.Element {
if (needsMigration) {
await asyncStorageService.migrateToSecureStorage();
}
await asyncStorageService.migrateShareExtensionCriticalFields();
};

useEffect(() => {
Expand Down
11 changes: 11 additions & 0 deletions src/services/AsyncStorageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,17 @@ class AsyncStorageService {
}
}

async migrateShareExtensionCriticalFields(): Promise<void> {
const alreadyMigrated = await secureStorageService.hasItem('xUser_rootFolderId');
if (alreadyMigrated) return;

const user = await this.getUser();
if (!user?.rootFolderId) return;

await this.saveItem(AsyncStorageKey.User, JSON.stringify(user));
logger.info('Share extension critical fields migrated (rootFolderId, bucket)');
}

async checkNeedsMigration(): Promise<{ needsMigration: boolean; itemsToMigrate: string[] }> {
const itemsToMigrate: string[] = [];

Expand Down
11 changes: 10 additions & 1 deletion src/services/SecureStorageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import * as SecureStore from 'expo-secure-store';
import { AsyncStorageKey } from '../types';
import { UserData, UserKeysHandler } from './UserKeysHandler';

const CRITICAL_USER_FIELDS = ['mnemonic', 'privateKey', 'publicKey', 'keys', 'revocationKey', 'revocateKey'];
const CRITICAL_USER_FIELDS = [
'mnemonic',
'privateKey',
'publicKey',
'keys',
'revocationKey',
'revocateKey',
'rootFolderId',
'bucket',
];

class SecureStorageService {
private readonly MAX_CHUNK_SIZE = 1800;
Expand Down
104 changes: 0 additions & 104 deletions src/shareExtension/AndroidShareScreen.tsx

This file was deleted.

Loading
Loading