Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revoke installations #480

Merged
merged 5 commits into from
Aug 23, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import expo.modules.xmtpreactnativesdk.wrappers.DecodedMessageWrapper
import expo.modules.xmtpreactnativesdk.wrappers.DecryptedLocalAttachment
import expo.modules.xmtpreactnativesdk.wrappers.EncryptedLocalAttachment
import expo.modules.xmtpreactnativesdk.wrappers.GroupWrapper
import expo.modules.xmtpreactnativesdk.wrappers.InboxStateWrapper
import expo.modules.xmtpreactnativesdk.wrappers.MemberWrapper
import expo.modules.xmtpreactnativesdk.wrappers.PermissionPolicySetWrapper
import expo.modules.xmtpreactnativesdk.wrappers.PreparedLocalMessage
Expand Down Expand Up @@ -65,6 +66,7 @@ import org.xmtp.proto.message.api.v1.MessageApiOuterClass
import org.xmtp.proto.message.contents.Invitation.ConsentProofPayload
import org.xmtp.proto.message.contents.PrivateKeyOuterClass
import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.GroupPermissionPreconfiguration
import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.InboxState
import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.PermissionOption
import java.io.BufferedReader
import java.io.File
Expand Down Expand Up @@ -274,6 +276,27 @@ class XMTPModule : Module() {
}
}

AsyncFunction("revokeAllOtherInstallations") Coroutine { inboxId: String ->
withContext(Dispatchers.IO) {
logV("revokeAllOtherInstallations")
val client = clients[inboxId] ?: throw XMTPException("No client")
val reactSigner =
ReactNativeSigner(module = this@XMTPModule, address = client.address)
signer = reactSigner

client.revokeAllOtherInstallations(reactSigner)
signer = null
}
}

AsyncFunction("getInboxState") Coroutine { inboxId: String, refreshFromNetwork: Boolean ->
withContext(Dispatchers.IO) {
val client = clients[inboxId] ?: throw XMTPException("No client")
val inboxState = client.inboxState(refreshFromNetwork)
InboxStateWrapper.encode(inboxState)
}
}

//
// Auth functions
//
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package expo.modules.xmtpreactnativesdk.wrappers

import com.google.gson.GsonBuilder
import uniffi.xmtpv3.org.xmtp.android.library.libxmtp.InboxState

class InboxStateWrapper {
companion object {
fun encodeToObj(inboxState: InboxState): Map<String, Any> {
return mapOf(
"inboxId" to inboxState.inboxId,
"addresses" to inboxState.addresses,
"installationIds" to inboxState.installationIds,
"recoveryAddress" to inboxState.recoveryAddress
)
}

fun encode(inboxState: InboxState): String {
val gson = GsonBuilder().create()
val obj = encodeToObj(inboxState)
return gson.toJson(obj)
}
}
}
38 changes: 38 additions & 0 deletions example/src/tests/groupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,44 @@
return true
})

test('can revoke all other installations', async () => {
const keyBytes = new Uint8Array([
233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64,
166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145,
])
const alixWallet = Wallet.createRandom()

const alix = await Client.create(alixWallet, {
env: 'local',
appVersion: 'Testing/0.0.0',
enableV3: true,
dbEncryptionKey: keyBytes,
})
await alix.deleteLocalDatabase()

const alix2 = await Client.create(alixWallet, {
env: 'local',
appVersion: 'Testing/0.0.0',
enableV3: true,
dbEncryptionKey: keyBytes,
})

const inboxState = await alix2.inboxState(true)
assert(
inboxState.installationIds.length === 2,
`installationIds length should be 2 but was ${inboxState.installationIds.length}`
)

await alix2.revokeAllOtherInstallations(alixWallet)

const inboxState2 = await alix2.inboxState(true)
assert(
inboxState2.installationIds.length === 1,
`installationIds length should be 1 but was ${inboxState2.installationIds.length}`
)
return true
})

test('calls preAuthenticateToInboxCallback when supplied', async () => {
let isCallbackCalled = 0
let isPreAuthCalled = false
Expand Down Expand Up @@ -949,7 +987,7 @@
throw Error('Unexpected num groups (should be 1): ' + groups.length)
}

assert(groups[0].members.length == 2, 'should be 2')

Check warning on line 990 in example/src/tests/groupTests.ts

View workflow job for this annotation

GitHub Actions / lint

Expected '===' and instead saw '=='

// bo creates a group with alix so a stream callback is fired
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
30 changes: 30 additions & 0 deletions ios/Wrappers/InboxStateWrapper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// InboxStateWrapper.swift
// XMTPReactNative
//
// Created by Naomi Plasterer on 8/21/24.
//

import Foundation
import XMTP

// Wrapper around XMTP.InboxState to allow passing these objects back into react native.
struct InboxStateWrapper {
static func encodeToObj(_ inboxState: XMTP.InboxState) throws -> [String: Any] {
return [
"inboxId": inboxState.inboxId,
"addresses": inboxState.addresses,
"installationIds": inboxState.installationIds,
"recoveryAddress": inboxState.recoveryAddress
]
}

static func encode(_ inboxState: XMTP.InboxState) throws -> String {
let obj = try encodeToObj(inboxState)
let data = try JSONSerialization.data(withJSONObject: obj)
guard let result = String(data: data, encoding: .utf8) else {
throw WrapperError.encodeError("could not encode inboxState")
}
return result
}
}
19 changes: 19 additions & 0 deletions ios/XMTPModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,25 @@ public class XMTPModule: Module {
}
try await client.requestMessageHistorySync()
}

AsyncFunction("revokeAllOtherInstallations") { (inboxId: String) in
guard let client = await clientsManager.getClient(key: inboxId) else {
throw Error.noClient
}
let signer = ReactNativeSigner(module: self, address: client.address)
self.signer = signer

try await client.revokeAllOtherInstallations(signingKey: signer)
self.signer = nil
}

AsyncFunction("getInboxState") { (inboxId: String, refreshFromNetwork: Bool) -> String in
guard let client = await clientsManager.getClient(key: inboxId) else {
throw Error.noClient
}
let inboxState = try await client.inboxState(refreshFromNetwork: refreshFromNetwork)
return try InboxStateWrapper.encode(inboxState)
}

//
// Auth functions
Expand Down
13 changes: 13 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from './lib/ConversationContainer'
import { DecodedMessage, MessageDeliveryStatus } from './lib/DecodedMessage'
import { Group, PermissionUpdateOption } from './lib/Group'
import { InboxState } from './lib/InboxState'
import { Member } from './lib/Member'
import type { Query } from './lib/Query'
import { ConversationSendPayload } from './lib/types'
Expand Down Expand Up @@ -70,6 +71,18 @@ export async function requestMessageHistorySync(inboxId: string) {
return XMTPModule.requestMessageHistorySync(inboxId)
}

export async function getInboxState(
inboxId: string,
refreshFromNetwork: boolean
): Promise<InboxState> {
const inboxState = await XMTPModule.getInboxState(inboxId, refreshFromNetwork)
return InboxState.from(inboxState)
}

export async function revokeAllOtherInstallations(inboxId: string) {
return XMTPModule.revokeAllOtherInstallations(inboxId)
}

export async function auth(
address: string,
environment: 'local' | 'dev' | 'production',
Expand Down
57 changes: 57 additions & 0 deletions src/lib/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
PreparedLocalMessage,
} from './ContentCodec'
import Conversations from './Conversations'
import { InboxState } from './InboxState'
import { TextCodec } from './NativeCodecs/TextCodec'
import { Query } from './Query'
import { Signer, getSigner } from './Signer'
Expand Down Expand Up @@ -455,6 +456,62 @@ export class Client<
return await XMTPModule.requestMessageHistorySync(this.inboxId)
}

/**
* Revoke all other installations but the current one.
*/
async revokeAllOtherInstallations(wallet: Signer | WalletClient | null) {
const signer = getSigner(wallet)
if (!signer) {
throw new Error('Signer is not configured')
}
return new Promise<void>((resolve, reject) => {
;(async () => {
Client.signSubscription = XMTPModule.emitter.addListener(
'sign',
async (message: { id: string; message: string }) => {
const request: { id: string; message: string } = message
try {
const signatureString = await signer.signMessage(request.message)
const eSig = splitSignature(signatureString)
const r = hexToBytes(eSig.r)
const s = hexToBytes(eSig.s)
const sigBytes = new Uint8Array(65)
sigBytes.set(r)
sigBytes.set(s, r.length)
sigBytes[64] = eSig.recoveryParam

const signature = Buffer.from(sigBytes).toString('base64')

await XMTPModule.receiveSignature(request.id, signature)
} catch (e) {
const errorMessage =
'ERROR in revokeInstallations. User rejected signature'
Client.signSubscription?.remove()
console.info(errorMessage, e)
reject(errorMessage)
}
}
)
await XMTPModule.revokeAllOtherInstallations(this.inboxId)
Client.signSubscription?.remove()
resolve()
})().catch((error) => {
Client.signSubscription?.remove()
reject(error)
})
})
}

/**
* Make a request for a inboxs state.
*
* @param {boolean} refreshFromNetwork - If you want to refresh the current state of in the inbox from the network or not.
* @returns {Promise<InboxState>} A Promise resolving to a InboxState.
*/
async inboxState(refreshFromNetwork: boolean): Promise<InboxState> {
return await XMTPModule.getInboxState(this.inboxId, refreshFromNetwork)
}

/**
* Determines whether the current user can send messages to the specified peers over groups.
*
Expand Down
30 changes: 30 additions & 0 deletions src/lib/InboxState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { InboxId } from './Client'

export class InboxState {
inboxId: InboxId
addresses: string[]
installationIds: string[]
recoveryAddress: string

constructor(
inboxId: InboxId,
addresses: string[],
installationIds: string[],
recoveryAddress: string
) {
this.inboxId = inboxId
this.addresses = addresses
this.installationIds = installationIds
this.recoveryAddress = recoveryAddress
}

static from(json: string): InboxState {
const entry = JSON.parse(json)
return new InboxState(
entry.inboxId,
entry.addresses,
entry.installationIds,
entry.recoveryAddress
)
}
}
Loading