diff --git a/android/build.gradle b/android/build.gradle index 015f1b152..748c76a0d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -95,7 +95,7 @@ repositories { dependencies { implementation project(':expo-modules-core') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}" - implementation "org.xmtp:android:0.6.20" + implementation "org.xmtp:android:0.7.0" implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.facebook.react:react-native:0.71.3' implementation "com.daveanthonythomas.moshipack:moshipack:1.0.1" diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt index 404470c08..c531c9016 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -27,6 +27,7 @@ import org.xmtp.android.library.Client import org.xmtp.android.library.ClientOptions import org.xmtp.android.library.ConsentState import org.xmtp.android.library.Conversation +import org.xmtp.android.library.PreEventCallback import org.xmtp.android.library.PreparedMessage import org.xmtp.android.library.SendOptions import org.xmtp.android.library.SigningKey @@ -128,7 +129,14 @@ class XMTPModule : Module() { override fun definition() = ModuleDefinition { Name("XMTP") - Events("sign", "authed", "conversation", "message") + Events( + "sign", + "authed", + "conversation", + "message", + "preEnableIdentityCallback", + "preCreateIdentityCallback" + ) Function("address") { clientAddress: String -> logV("address") @@ -139,11 +147,19 @@ class XMTPModule : Module() { // // Auth functions // - AsyncFunction("auth") { address: String, environment: String, appVersion: String? -> + AsyncFunction("auth") { address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean? -> logV("auth") val reactSigner = ReactNativeSigner(module = this@XMTPModule, address = address) signer = reactSigner - val options = ClientOptions(api = apiEnvironments(environment, appVersion)) + val preCreateIdentityCallback: PreEventCallback? = + preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true } + val preEnableIdentityCallback: PreEventCallback? = + preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true } + val options = ClientOptions( + api = apiEnvironments(environment, appVersion), + preCreateIdentityCallback = preCreateIdentityCallback, + preEnableIdentityCallback = preEnableIdentityCallback + ) clients[address] = Client().create(account = reactSigner, options = options) ContentJson.Companion signer = null @@ -156,10 +172,19 @@ class XMTPModule : Module() { } // Generate a random wallet and set the client to that - AsyncFunction("createRandom") { environment: String, appVersion: String? -> + AsyncFunction("createRandom") { environment: String, appVersion: String?, hasCreateIdentityCallback: Boolean?, hasEnableIdentityCallback: Boolean? -> logV("createRandom") val privateKey = PrivateKeyBuilder() - val options = ClientOptions(api = apiEnvironments(environment, appVersion)) + val preCreateIdentityCallback: PreEventCallback? = + preCreateIdentityCallback.takeIf { hasCreateIdentityCallback == true } + val preEnableIdentityCallback: PreEventCallback? = + preEnableIdentityCallback.takeIf { hasEnableIdentityCallback == true } + + val options = ClientOptions( + api = apiEnvironments(environment, appVersion), + preCreateIdentityCallback = preCreateIdentityCallback, + preEnableIdentityCallback = preEnableIdentityCallback + ) val randomClient = Client().create(account = privateKey, options = options) ContentJson.Companion clients[randomClient.address] = randomClient @@ -687,6 +712,14 @@ class XMTPModule : Module() { Log.v("XMTPModule", msg) } } + + private val preEnableIdentityCallback: suspend () -> Unit = { + sendEvent("preEnableIdentityCallback") + } + + private val preCreateIdentityCallback: suspend () -> Unit = { + sendEvent("preCreateIdentityCallback") + } } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 57bf7ac76..257d96291 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -411,7 +411,7 @@ PODS: - GenericJSON (~> 2.0) - Logging (~> 1.0.0) - secp256k1.swift (~> 0.1) - - XMTP (0.7.2-alpha0): + - XMTP (0.7.3-alpha0): - Connect-Swift (= 0.3.0) - GzipSwift - web3.swift @@ -419,7 +419,7 @@ PODS: - XMTPReactNative (0.1.0): - ExpoModulesCore - MessagePacker - - XMTP (= 0.7.2-alpha0) + - XMTP (= 0.7.3-alpha0) - XMTPRust (0.3.7-beta0) - Yoga (1.14.0) @@ -668,11 +668,11 @@ SPEC CHECKSUMS: secp256k1.swift: a7e7a214f6db6ce5db32cc6b2b45e5c4dd633634 SwiftProtobuf: b02b5075dcf60c9f5f403000b3b0c202a11b6ae1 web3.swift: 2263d1e12e121b2c42ffb63a5a7beb1acaf33959 - XMTP: 4930b80dc99a6a8ebcf1f292a162c1f316f78c50 - XMTPReactNative: 68c723488857950d10fc8ee969de0baae8f9b2ca + XMTP: dc02c96b475e326a4a7b3d3912cc45cf3527bd0b + XMTPReactNative: 5c1111c5bd3456e75b3fa67d1ddccabb7a01df11 XMTPRust: 8848a2ba761b2c961d666632f2ad27d1082faa93 Yoga: e71803b4c1fff832ccf9b92541e00f9b873119b9 PODFILE CHECKSUM: 522d88edc2d5fac4825e60a121c24abc18983367 -COCOAPODS: 1.14.3 +COCOAPODS: 1.13.0 diff --git a/example/src/tests.ts b/example/src/tests.ts index 6ee539f82..0c59eb60e 100644 --- a/example/src/tests.ts +++ b/example/src/tests.ts @@ -751,3 +751,37 @@ test('register and use custom content types', async () => { return true }) + +test('calls preCreateIdentityCallback when supplied', async () => { + let isCallbackCalled = false + const preCreateIdentityCallback = () => { + isCallbackCalled = true + } + await Client.createRandom({ + env: 'local', + preCreateIdentityCallback, + }) + + if (!isCallbackCalled) { + throw new Error('preCreateIdentityCallback not called') + } + + return isCallbackCalled +}) + +test('calls preEnableIdentityCallback when supplied', async () => { + let isCallbackCalled = false + const preEnableIdentityCallback = () => { + isCallbackCalled = true + } + await Client.createRandom({ + env: 'local', + preEnableIdentityCallback, + }) + + if (!isCallbackCalled) { + throw new Error('preEnableIdentityCallback not called') + } + + return isCallbackCalled +}) diff --git a/ios/XMTPModule.swift b/ios/XMTPModule.swift index 7a086f37d..8387ab56d 100644 --- a/ios/XMTPModule.swift +++ b/ios/XMTPModule.swift @@ -51,7 +51,7 @@ public class XMTPModule: Module { public func definition() -> ModuleDefinition { Name("XMTP") - Events("sign", "authed", "conversation", "message") + Events("sign", "authed", "conversation", "message", "preEnableIdentityCallback", "preCreateIdentityCallback") AsyncFunction("address") { (clientAddress: String) -> String in if let client = await clientsManager.getClient(key: clientAddress) { @@ -64,10 +64,12 @@ public class XMTPModule: Module { // // Auth functions // - AsyncFunction("auth") { (address: String, environment: String, appVersion: String?) in + AsyncFunction("auth") { (address: String, environment: String, appVersion: String?, hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?) in let signer = ReactNativeSigner(module: self, address: address) self.signer = signer - let options = createClientConfig(env: environment, appVersion: appVersion) + let preCreateIdentityCallback: PreEventCallback? = hasCreateIdentityCallback ?? false ? self.preCreateIdentityCallback : nil + let preEnableIdentityCallback: PreEventCallback? = hasEnableIdentityCallback ?? false ? self.preEnableIdentityCallback : nil + let options = createClientConfig(env: environment, appVersion: appVersion, preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback) try await clientsManager.updateClient(key: address, client: await XMTP.Client.create(account: signer, options: options)) self.signer = nil sendEvent("authed") @@ -78,9 +80,12 @@ public class XMTPModule: Module { } // Generate a random wallet and set the client to that - AsyncFunction("createRandom") { (environment: String, appVersion: String?) -> String in + AsyncFunction("createRandom") { (environment: String, appVersion: String?, hasCreateIdentityCallback: Bool?, hasEnableIdentityCallback: Bool?) -> String in let privateKey = try PrivateKey.generate() - let options = createClientConfig(env: environment, appVersion: appVersion) + let preCreateIdentityCallback: PreEventCallback? = hasCreateIdentityCallback ?? false ? self.preCreateIdentityCallback : nil + let preEnableIdentityCallback: PreEventCallback? = hasEnableIdentityCallback ?? false ? self.preEnableIdentityCallback : nil + + let options = createClientConfig(env: environment, appVersion: appVersion, preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback) let client = try await Client.create(account: privateKey, options: options) await clientsManager.updateClient(key: client.address, client: client) @@ -146,14 +151,14 @@ public class XMTPModule: Module { return try await client.canMessage(peerAddress) } - AsyncFunction("staticCanMessage") { (peerAddress: String, environment: String, appVersion: String?) -> Bool in - do { - let options = createClientConfig(env: environment, appVersion: appVersion) - return try await XMTP.Client.canMessage(peerAddress, options: options) - } catch { - throw Error.noClient - } - } + AsyncFunction("staticCanMessage") { (peerAddress: String, environment: String, appVersion: String?) -> Bool in + do { + let options = createClientConfig(env: environment, appVersion: appVersion) + return try await XMTP.Client.canMessage(peerAddress, options: options) + } catch { + throw Error.noClient + } + } AsyncFunction("encryptAttachment") { (clientAddress: String, fileJson: String) -> String in guard let client = await clientsManager.getClient(key: clientAddress) else { @@ -506,36 +511,36 @@ public class XMTPModule: Module { throw Error.noClient } let consentList = try await client.contacts.refreshConsentList() - - return try consentList.entries.compactMap { entry in - try ConsentWrapper.encode(entry.value) - } + + return try consentList.entries.compactMap { entry in + try ConsentWrapper.encode(entry.value) + } } AsyncFunction("conversationConsentState") { (clientAddress: String, conversationTopic: String) -> String in guard let conversation = try await findConversation(clientAddress: clientAddress, topic: conversationTopic) else { throw Error.conversationNotFound(conversationTopic) } - return ConsentWrapper.consentStateToString(state: await conversation.consentState()) + return ConsentWrapper.consentStateToString(state: await conversation.consentState()) } - AsyncFunction("consentList") { (clientAddress: String) -> [String] in - guard let client = await clientsManager.getClient(key: clientAddress) else { - throw Error.noClient - } - let entries = await client.contacts.consentList.entries - - return try entries.compactMap { entry in - try ConsentWrapper.encode(entry.value) - } - } + AsyncFunction("consentList") { (clientAddress: String) -> [String] in + guard let client = await clientsManager.getClient(key: clientAddress) else { + throw Error.noClient + } + let entries = await client.contacts.consentList.entries + + return try entries.compactMap { entry in + try ConsentWrapper.encode(entry.value) + } + } } // // Helpers // - func createClientConfig(env: String, appVersion: String?) -> XMTP.ClientOptions { + func createClientConfig(env: String, appVersion: String?, preEnableIdentityCallback: PreEventCallback? = nil, preCreateIdentityCallback: PreEventCallback? = nil) -> XMTP.ClientOptions { // Ensure that all codecs have been registered. switch env { case "local": @@ -543,19 +548,19 @@ public class XMTPModule: Module { env: XMTP.XMTPEnvironment.local, isSecure: false, appVersion: appVersion - )) + ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback) case "production": return XMTP.ClientOptions(api: XMTP.ClientOptions.Api( env: XMTP.XMTPEnvironment.production, isSecure: true, appVersion: appVersion - )) + ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback) default: return XMTP.ClientOptions(api: XMTP.ClientOptions.Api( env: XMTP.XMTPEnvironment.dev, isSecure: true, appVersion: appVersion - )) + ), preEnableIdentityCallback: preEnableIdentityCallback, preCreateIdentityCallback: preCreateIdentityCallback) } } @@ -665,4 +670,12 @@ public class XMTPModule: Module { func getConversationsKey(clientAddress: String) -> String { return "conversations:\(clientAddress)" } + + func preEnableIdentityCallback() { + sendEvent("preEnableIdentityCallback") + } + + func preCreateIdentityCallback() { + sendEvent("preCreateIdentityCallback") + } } diff --git a/ios/XMTPReactNative.podspec b/ios/XMTPReactNative.podspec index 24024418e..88246e716 100644 --- a/ios/XMTPReactNative.podspec +++ b/ios/XMTPReactNative.podspec @@ -25,5 +25,5 @@ Pod::Spec.new do |s| s.source_files = "**/*.{h,m,swift}" s.dependency "MessagePacker" - s.dependency "XMTP", "= 0.7.2-alpha0" + s.dependency "XMTP", "= 0.7.3-alpha0" end diff --git a/src/index.ts b/src/index.ts index 3005ea791..5e324ad8b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,9 +33,17 @@ export function address(): string { export async function auth( address: string, environment: 'local' | 'dev' | 'production', - appVersion?: string | undefined + appVersion?: string | undefined, + hasCreateIdentityCallback?: boolean | undefined, + hasEnableIdentityCallback?: boolean | undefined ) { - return await XMTPModule.auth(address, environment, appVersion) + return await XMTPModule.auth( + address, + environment, + appVersion, + hasCreateIdentityCallback, + hasEnableIdentityCallback + ) } export async function receiveSignature(requestID: string, signature: string) { @@ -44,9 +52,16 @@ export async function receiveSignature(requestID: string, signature: string) { export async function createRandom( environment: 'local' | 'dev' | 'production', - appVersion?: string | undefined + appVersion?: string | undefined, + hasCreateIdentityCallback?: boolean | undefined, + hasEnableIdentityCallback?: boolean | undefined ): Promise { - return await XMTPModule.createRandom(environment, appVersion) + return await XMTPModule.createRandom( + environment, + appVersion, + hasCreateIdentityCallback, + hasEnableIdentityCallback + ) } export async function createFromKeyBundle( diff --git a/src/lib/Client.ts b/src/lib/Client.ts index cd1089182..c6a45c96d 100644 --- a/src/lib/Client.ts +++ b/src/lib/Client.ts @@ -1,4 +1,5 @@ import { Signer, utils } from 'ethers' +import { Subscription } from 'expo-modules-core' import Contacts from './Contacts' import type { @@ -49,6 +50,8 @@ export class Client { > > { const options = defaultOptions(opts) + const { enableSubscription, createSubscription } = + this.setupSubscriptions(options) return new Promise< Client< ExtractDecodedType<[...ContentCodecs, TextCodec][number]> | undefined @@ -81,9 +84,13 @@ export class Client { XMTPModule.auth( await signer.getAddress(), options.env, - options.appVersion + options.appVersion, + Boolean(createSubscription), + Boolean(enableSubscription) ) })() + this.removeSubscription(enableSubscription) + this.removeSubscription(createSubscription) }) } @@ -103,11 +110,17 @@ export class Client { > > { const options = defaultOptions(opts) - + const { enableSubscription, createSubscription } = + this.setupSubscriptions(options) const address = await XMTPModule.createRandom( options.env, - options.appVersion + options.appVersion, + Boolean(createSubscription), + Boolean(enableSubscription) ) + this.removeSubscription(enableSubscription) + this.removeSubscription(createSubscription) + return new Client(address, opts?.codecs || []) } @@ -174,6 +187,61 @@ export class Client { ) } + private static addSubscription( + event: string, + opts: ClientOptions, + callback: () => Promise | void + ): Subscription | undefined { + if (this.hasEventCallback(event, opts)) { + return XMTPModule.emitter.addListener(event, callback) + } + return undefined + } + + private static async executeCallback( + callback?: () => Promise | void + ): Promise { + await callback?.() + } + + private static hasEventCallback( + event: string, + opts: CallbackOptions + ): boolean { + return opts?.[event] !== undefined + } + + private static async removeSubscription( + subscription?: Subscription + ): Promise { + if (subscription) { + subscription.remove() + } + } + + private static setupSubscriptions(opts: ClientOptions): { + enableSubscription?: Subscription + createSubscription?: Subscription + } { + const enableSubscription = this.addSubscription( + 'preEnableIdentityCallback', + opts, + async () => { + await this.executeCallback(opts?.preEnableIdentityCallback) + } + ) + + const createSubscription = this.addSubscription( + 'preCreateIdentityCallback', + opts, + async () => { + await this.executeCallback(opts?.preCreateIdentityCallback) + } + ) + + return { enableSubscription, createSubscription } + } + constructor( address: string, codecs: XMTPModule.ContentCodec[] = [] @@ -282,7 +350,7 @@ export class Client { } } -export type ClientOptions = NetworkOptions +export type ClientOptions = NetworkOptions & CallbackOptions export type NetworkOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) @@ -301,6 +369,11 @@ export type NetworkOptions = { appVersion?: string } +export type CallbackOptions = { + preCreateIdentityCallback?: () => Promise | void + preEnableIdentityCallback?: () => Promise | void +} + /** * Provide a default client configuration. These settings can be used on their own, or as a starting point for custom configurations *