diff --git a/android/build.gradle b/android/build.gradle index 685cd6ac9..5a9df29a2 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:4.9.0" + implementation "org.xmtp:android:4.10.0-rc2" 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 98683e725..53a935341 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/XMTPModule.kt @@ -13,6 +13,7 @@ import expo.modules.kotlin.functions.Coroutine import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.xmtpreactnativesdk.wrappers.ArchiveMetadataWrapper +import expo.modules.xmtpreactnativesdk.wrappers.AvailableArchiveWrapper import expo.modules.xmtpreactnativesdk.wrappers.AuthParamsWrapper import expo.modules.xmtpreactnativesdk.wrappers.ClientWrapper import expo.modules.xmtpreactnativesdk.wrappers.ConsentWrapper @@ -226,12 +227,6 @@ class XMTPModule : Module() { dbEncryptionKey.foldIndexed(ByteArray(dbEncryptionKey.size)) { i, a, v -> a.apply { set(i, v.toByte()) } } - val historySyncUrl = authOptions.historySyncUrl - ?: when (authOptions.environment) { - "production" -> "https://message-history.production.ephemera.network/" - "local" -> "http://10.0.2.2:5558" - else -> "https://message-history.dev.ephemera.network/" - } return ClientOptions( api = apiEnvironments( authOptions.environment, @@ -243,7 +238,6 @@ class XMTPModule : Module() { appContext = context, dbEncryptionKey = encryptionKeyBytes, dbDirectory = authOptions.dbDirectory, - historySyncUrl = historySyncUrl, deviceSyncEnabled = authOptions.deviceSyncEnabled, forkRecoveryOptions = authOptions.forkRecoveryOptions ) @@ -2177,6 +2171,39 @@ class XMTPModule : Module() { ArchiveMetadataWrapper.encode(metadata) } } + + AsyncFunction("sendSyncArchive") Coroutine { installationId: String, pin: String, serverUrl: String?, startNs: Long?, endNs: Long?, archiveElements: List?, excludeDisappearingMessages: Boolean? -> + withContext(Dispatchers.IO) { + val client = clients[installationId] ?: throw XMTPException("No client") + val elements = archiveElements?.map { getArchiveElement(it) } ?: listOf(ArchiveElement.MESSAGES, ArchiveElement.CONSENT) + val opts = ArchiveOptions(startNs, endNs, elements, excludeDisappearingMessages ?: false) + val url = serverUrl?.takeIf { it.isNotBlank() } ?: client.environment.getHistorySyncUrl() + client.sendSyncArchive(opts, url, pin) + } + } + + AsyncFunction("processSyncArchive") Coroutine { installationId: String, archivePin: String? -> + withContext(Dispatchers.IO) { + val client = clients[installationId] ?: throw XMTPException("No client") + client.processSyncArchive(archivePin) + } + } + + AsyncFunction("listAvailableArchives") Coroutine { installationId: String, daysCutoff: Long -> + withContext(Dispatchers.IO) { + val client = clients[installationId] ?: throw XMTPException("No client") + val archives = client.listAvailableArchives(daysCutoff) + AvailableArchiveWrapper.encodeList(archives) + } + } + + AsyncFunction("syncAllDeviceSyncGroups") Coroutine { installationId: String -> + withContext(Dispatchers.IO) { + val client = clients[installationId] ?: throw XMTPException("No client") + val summary = client.syncAllDeviceSyncGroups() + GroupSyncSummaryWrapper.encode(summary) + } + } } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ArchiveMetadataWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ArchiveMetadataWrapper.kt index 64e21334a..7142c9e47 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ArchiveMetadataWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/ArchiveMetadataWrapper.kt @@ -32,11 +32,11 @@ class ArchiveMetadataWrapper { } } - fun encode(metadata: ArchiveMetadata?): String { - val obj = if (metadata != null) { + /** Returns metadata as a map for embedding in another JSON object (avoids double JSON encoding). */ + fun encodeToMap(metadata: ArchiveMetadata?): Map { + return if (metadata != null) { encodeToObj(metadata) } else { - // Create a default metadata object when null mapOf( "archiveVersion" to 0u, "elements" to listOf("messages", "consent"), @@ -45,7 +45,10 @@ class ArchiveMetadataWrapper { "endNs" to null ) } - return gson.toJson(obj) + } + + fun encode(metadata: ArchiveMetadata?): String { + return gson.toJson(encodeToMap(metadata)) } } } diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt index 9284ba796..ac4895f77 100644 --- a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AuthParamsWrapper.kt @@ -10,7 +10,6 @@ import java.math.BigInteger class AuthParamsWrapper( val environment: String, val dbDirectory: String?, - val historySyncUrl: String?, val customLocalUrl: String?, val deviceSyncEnabled: Boolean, val debugEventsEnabled: Boolean, @@ -83,7 +82,6 @@ class AuthParamsWrapper( return AuthParamsWrapper( jsonOptions.get("environment").asString, if (jsonOptions.has("dbDirectory")) jsonOptions.get("dbDirectory").asString else null, - if (jsonOptions.has("historySyncUrl")) jsonOptions.get("historySyncUrl").asString else null, if (jsonOptions.has("customLocalUrl")) jsonOptions.get("customLocalUrl").asString else null, if (jsonOptions.has("deviceSyncEnabled")) jsonOptions.get("deviceSyncEnabled").asBoolean else true, if (jsonOptions.has("debugEventsEnabled")) jsonOptions.get("debugEventsEnabled").asBoolean else false, diff --git a/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AvailableArchiveWrapper.kt b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AvailableArchiveWrapper.kt new file mode 100644 index 000000000..e437a936c --- /dev/null +++ b/android/src/main/java/expo/modules/xmtpreactnativesdk/wrappers/AvailableArchiveWrapper.kt @@ -0,0 +1,22 @@ +package expo.modules.xmtpreactnativesdk.wrappers + +import android.util.Base64 +import com.google.gson.GsonBuilder +import org.xmtp.android.library.libxmtp.AvailableArchive + +class AvailableArchiveWrapper { + companion object { + private val gson = GsonBuilder().create() + + fun encodeToObj(archive: AvailableArchive): Map = mapOf( + "pin" to archive.pin, + "metadata" to ArchiveMetadataWrapper.encodeToMap(archive.metadata), + "sentByInstallation" to Base64.encodeToString(archive.sentByInstallation, Base64.NO_WRAP), + ) + + fun encodeList(archives: List): String { + val list = archives.map { encodeToObj(it) } + return gson.toJson(list) + } + } +} diff --git a/example/EXAMPLE.env b/example/EXAMPLE.env index 5c653dc31..8b4bdf8a2 100644 --- a/example/EXAMPLE.env +++ b/example/EXAMPLE.env @@ -1,2 +1,3 @@ TEST_PRIVATE_KEY=INSERT_TEST_PRIVATE_KEY_HERE THIRD_WEB_CLIENT_ID=INSERT_CLIENT_ID_HERE +GATEWAY_HOST=D14N_GATEWAY_PAYER_HOST_URL_HERE \ No newline at end of file diff --git a/example/app.json b/example/app.json index a2880d743..cffe21ad3 100644 --- a/example/app.json +++ b/example/app.json @@ -22,7 +22,8 @@ "cameraPermission": "The app accesses your camera to let you attach photos as messages." } ], - "expo-secure-store" + "expo-secure-store", + "./plugins/withReactNativeConfigEnv.js" ], "ios": { "supportsTablet": true, diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 92003bd92..390a27995 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - boost (1.84.0) - - Connect-Swift (1.2.0): + - Connect-Swift (1.2.1): - SwiftProtobuf (~> 1.30.0) - CryptoSwift (1.8.3) - CSecp256k1 (0.2.0) @@ -1757,7 +1757,7 @@ PODS: - SQLCipher/standard (4.5.7): - SQLCipher/common - SwiftProtobuf (1.30.0) - - XMTP (4.9.0): + - XMTP (4.10.0-rc2): - Connect-Swift (~> 1.2.0) - CryptoSwift (= 1.8.3) - SQLCipher (= 4.5.7) @@ -1766,7 +1766,7 @@ PODS: - ExpoModulesCore - MessagePacker - SQLCipher (= 4.5.7) - - XMTP (= 4.9.0) + - XMTP (= 4.10.0-rc2) - Yoga (0.0.0) DEPENDENCIES: @@ -2077,7 +2077,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 1dca942403ed9342f98334bf4c3621f011aa7946 - Connect-Swift: 82bcc0834587bd537f17a9720f62ea9fc7d9f3a5 + Connect-Swift: 8550afdb45aaea9f527d29c74f7224bf78e2bc26 CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 CSecp256k1: 2a59c03e52637ded98896a33be4b2649392cb843 DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 @@ -2179,8 +2179,8 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SQLCipher: 5e6bfb47323635c8b657b1b27d25c5f1baf63bf5 SwiftProtobuf: 3697407f0d5b23bedeba9c2eaaf3ec6fdff69349 - XMTP: 322f5be971dca2b1f402727ffda2f62d4ab7f71d - XMTPReactNative: 44d7e20a4affcf6e3c7c62c2331c2dcb9696c934 + XMTP: 40a323abd37322a4d0c323a1fd97e9a67ce9c16f + XMTPReactNative: f62ff8302fb02c611cae0be0981021d6f3426036 Yoga: 40f19fff64dce86773bf8b602c7070796c007970 PODFILE CHECKSUM: c76510e65e7d9673f44024ae2d0a10eec063a555 diff --git a/example/plugins/withReactNativeConfigEnv.js b/example/plugins/withReactNativeConfigEnv.js new file mode 100644 index 000000000..76091e58b --- /dev/null +++ b/example/plugins/withReactNativeConfigEnv.js @@ -0,0 +1,34 @@ +/** + * Expo config plugin: injects react-native-config's dotenv.gradle into the Android app build + * so that .env is loaded into BuildConfig and Config.* works on Android (same as iOS). + * Without this, env vars work on iOS but not Android after prebuild. + * + * @see https://github.com/luggit/react-native-config#android + */ +const { withAppBuildGradle } = require('expo/config-plugins') + +const DOTENV_LINE = + 'apply from: project.file("../../node_modules/react-native-config/android/dotenv.gradle")' +const COMMENT = + '// Load .env into BuildConfig so react-native-config Config.* works on Android (required for env vars in JS)' + +function withReactNativeConfigEnv(config) { + return withAppBuildGradle(config, (config) => { + const contents = config.modResults.contents + if (contents.includes('react-native-config/android/dotenv.gradle')) { + return config + } + const afterPlugins = contents.indexOf('apply plugin: "com.facebook.react"') + if (afterPlugins === -1) { + return config + } + const insertAt = contents.indexOf('\n', afterPlugins) + 1 + const before = contents.slice(0, insertAt) + const after = contents.slice(insertAt) + config.modResults.contents = + before + '\n' + COMMENT + '\n' + DOTENV_LINE + '\n\n' + after + return config + }) +} + +module.exports = withReactNativeConfigEnv diff --git a/example/src/LaunchScreen.tsx b/example/src/LaunchScreen.tsx index a796459f8..a9263db0c 100644 --- a/example/src/LaunchScreen.tsx +++ b/example/src/LaunchScreen.tsx @@ -1,6 +1,6 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' // import { ConnectWallet, useSigner } from '@thirdweb-dev/react-native' -import React, { useCallback, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { Button, ScrollView, @@ -18,6 +18,7 @@ import { NavigationParamList } from './Navigation' import { TestCategory } from './TestScreen' import { supportedCodecs } from './contentTypes/contentTypes' import { getDbEncryptionKey } from './hooks' +import { setTestEnv, type TestEnvOption } from './testEnv' // Custom Modal Picker Component const CustomPicker = ({ @@ -105,9 +106,15 @@ export default function LaunchScreen( const [selectedTest, setSelectedTest] = useState( TestCategory.all ) + const [selectedTestEnv, setSelectedTestEnv] = useState('local') const [selectedNetwork, setSelectedNetwork] = useState< 'dev' | 'local' | 'production' >('dev') + + useEffect(() => { + setTestEnv(selectedTestEnv) + }, [selectedTestEnv]) + // const signer = useSigner() // const [signerAddressDisplay, setSignerAddressDisplay] = useState() const { setClient } = useXmtp() @@ -170,6 +177,12 @@ export default function LaunchScreen( label, })) + const testEnvOptions: { id: TestEnvOption; label: string }[] = [ + { id: 'local', label: 'local' }, + { id: 'dev', label: 'dev' }, + { id: 'd14n', label: 'd14n (dev + gateway)' }, + ] + return ( Automated Tests @@ -182,6 +195,23 @@ export default function LaunchScreen( placeholder="Select Test" /> + + Test environment: + o.id === selectedTestEnv)?.label ?? + selectedTestEnv + } + onValueChange={(value) => { + const option = testEnvOptions.find((o) => o.label === value) + const env = (option?.id ?? value) as TestEnvOption + setSelectedTestEnv(env) + setTestEnv(env) + }} + options={testEnvOptions} + placeholder="Test environment" + /> +