import { PortalMobileMpc } from '@portal-hq/core/types' import { IPortalApi, type Storage, sdkLogger } from '@portal-hq/utils' import { Platform } from 'react-native' import type { ConfigureParams, GDriveStorage, GoogleErrorResult, GoogleSignInCanceledResult, StorageOptions, User, } from '../types' import { PortalMobileMpcModule } from './NativeModule' import GDrive from './gdrive' import GoogleSignIn, { statusCodes } from './signin' export class GoogleStorage implements Storage { public api?: IPortalApi public auth = GoogleSignIn public drive?: GDriveStorage public filename = '' public folder: string public mpc: PortalMobileMpc public user?: User constructor(opts: StorageOptions) { this.mpc = PortalMobileMpcModule as PortalMobileMpc this.folder = opts.folder || '_PORTAL_MPC_DO_NOT_DELETE_' const googleSignInConfig = { scopes: ['https://www.googleapis.com/auth/drive.file'], } as ConfigureParams if (Platform.OS === 'ios') { googleSignInConfig.iosClientId = opts.iosClientId } else if (Platform.OS === 'android') { googleSignInConfig.webClientId = opts.androidClientId } this.auth.configure(googleSignInConfig) this.drive = new GDrive({}) // Verify that the user is logged in before setting the user property this.auth .isSignedIn() .then((isSignedIn) => { if (isSignedIn) { this.user = this.auth.user || undefined } }) .catch((error) => { sdkLogger.warn( '[Portal GoogleStorage] Error checking sign-in status:', error, ) }) } /** * Public functions */ public async delete(): Promise { await this.assignAccessToken() const name = await this.getFilename() const fileId = await this.drive?.getIdForFilename(name) if (!fileId) { sdkLogger.info( '[Portal GoogleStorage] File could not be found. Skipping delete.', ) return false } await this.drive?.delete(fileId) await this.signOut() return true } public async getFilename(): Promise { if (!this.api) { throw new Error( '[Portal GoogleStorage] Cannot determine filename without a configured API', ) } if (!this.filename || !this.filename.length) { const clientData = await this.api.getClient() if (!clientData) { throw new Error( `[Portal GoogleStorage] Unable to fetch client data for API key.`, ) } const baseString = JSON.stringify({ custodianId: clientData.custodian.id, clientId: clientData.id, }) const name = await this.getExistingFileHash(baseString) this.filename = `${name}.txt` } return this.filename } public async read(): Promise { await this.assignAccessToken() const name = await this.getFilename() const fileId = await this.drive?.getIdForFilename(name) if (!fileId) { return '' } const contents = await this.drive?.read(fileId) if (!contents) { return '' } return contents } public async write(privateKey: string): Promise { await this.assignAccessToken() const filename = await this.getFilename() const fileId = await this.drive?.write(this.folder, filename, privateKey) if (!fileId) { throw new Error('[Portal GoogleStorage] Unable to write to Google Drive') } return fileId } public async validateOperations(): Promise { await this.assignAccessToken() const passed = await this.drive?.validateOperations(this.folder) return !!passed } /** * Private functions */ private async assignAccessToken() { const isSignedIn = await this.isSignedIn() if (!isSignedIn) { const user = await this.signIn() if ((user as GoogleSignInCanceledResult).cancelled) { sdkLogger.warn('[Portal GoogleStorage] User cancelled sign in request.') return } else if ((user as GoogleErrorResult).error) { sdkLogger.error( `[Portal GoogleStorage] Something failed in Google: ${ (user as GoogleErrorResult).error }`, ) throw (user as GoogleErrorResult).error } } const tokens = await this.auth.getTokens() if (tokens?.accessToken && this.drive) { this.drive.accessToken = tokens.accessToken } } public async isSignedIn(): Promise { try { const isSignedIn = await this.auth.isSignedIn() if (!isSignedIn) { return false } this.user = this.auth.user || undefined const tokens = await this.auth.getTokens() if (!tokens) { return false } return true } catch (error) { sdkLogger.warn( '[Portal GoogleStorage] Error checking sign-in status:', error, ) return false } } public async signIn(): Promise< User | GoogleSignInCanceledResult | GoogleErrorResult > { try { await this.auth.hasPlayServices() this.user = await this.auth.signIn() return this.user } catch (error: any) { switch (error.code) { case statusCodes.SIGN_IN_CANCELLED: return { cancelled: true } default: return { error } } } } public async signOut(): Promise { if (this.drive) { this.drive.accessToken = undefined } this.user = undefined await this.auth.signOut() } private async getExistingFileHash(baseString: string): Promise { const hashesResponseString = await this.mpc.getCustodianIdClientIdHashes(baseString) const hashesResponse: { data?: { android: string default: string ios: string react_native: string web_sdk: string } error?: { id: string message: string } } = JSON.parse(hashesResponseString) if (hashesResponse.error) { sdkLogger.error( '[Portal GoogleStorage] Error from binary.getCustodianIdClientIdHashes', { errorId: hashesResponse.error.id, errorMessage: hashesResponse.error.message, }, ) throw new Error(hashesResponse.error.message) } if (!hashesResponse.data) { throw new Error( `[Portal GoogleStorage] No data returned from binary.getCustodianIdClientIdHashes`, ) } // Create a set of unique hashes const uniqueHashes = new Set(Object.values(hashesResponse.data)) // Check for existing files and return early if found for (const hash of uniqueHashes) { const filename = `${hash}.txt` const fileId = await this.drive!.getIdForFilename(filename) if (fileId) { return hash } } // If no existing file found, use default hash for new files. return hashesResponse.data.default } } export default GoogleStorage