Update Twilio SDK to 6.2.0 and fix compatibility issues

This commit is contained in:
lengsukq 2025-11-17 17:28:44 +08:00
parent 4aebc41fec
commit 720cbbad99
14 changed files with 1145 additions and 87 deletions

File diff suppressed because one or more lines are too long

BIN
dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

37
dist/index.html vendored Normal file
View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>TwilioDemo</title>
<!-- The `react-native-web` recommended style reset: https://necolas.github.io/react-native-web/docs/setup/#root-element -->
<style id="expo-reset">
/* These styles make the body full-height */
html,
body {
height: 100%;
}
/* These styles disable body scrolling if you are using <ScrollView> */
body {
overflow: hidden;
}
/* These styles make the root element full-height */
#root {
display: flex;
height: 100%;
flex: 1;
}
</style>
<link rel="icon" href="/favicon.ico" /></head>
<body>
<!-- Use static rendering with Expo Router to support running without JavaScript. -->
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<!-- The root element for your Expo app. -->
<div id="root"></div>
<script src="/_expo/static/js/web/index-88190fe3c7e11f155ccc52a72d821e73.js" defer></script>
</body>
</html>

1
dist/metadata.json vendored Normal file
View File

@ -0,0 +1 @@
{"version":0,"bundler":"metro","fileMetadata":{}}

View File

@ -31,6 +31,12 @@ if (useManagedAndroidSdkVersions) {
}
}
allprojects {
repositories {
mavenCentral()
}
}
android {
namespace "expo.modules.mynativemodule"
defaultConfig {
@ -43,6 +49,5 @@ android {
}
dependencies {
// Twilio AAR文件依赖
implementation(files("libs/twilio-conversations-android-6.0.4.aar"))
implementation 'com.twilio:conversations-android:6.2.0'
}

View File

@ -3,14 +3,32 @@ package expo.modules.mynativemodule
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import expo.modules.kotlin.Promise
import java.net.URL
import expo.modules.kotlin.AppContext
import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
// Twilio SDK 6.2.0 导入
import com.twilio.conversations.ConversationsClient
import com.twilio.conversations.Conversation
import com.twilio.conversations.ErrorInfo
import com.twilio.conversations.CallbackListener
import com.twilio.conversations.ConversationsClientListener
import com.twilio.conversations.User
import com.twilio.conversations.StatusListener
class MyNativeModule : Module() {
// 模拟初始化状态
// Twilio Conversations 客户端
private var conversationsClient: ConversationsClient? = null
// 初始化状态
private var isInitialized = false
// 模拟对话列表
private val mockConversations = mutableListOf<Map<String, Any>>()
// 应用上下文
private val context: Context
get() = appContext.reactContext ?: throw IllegalStateException("React Application Context is not available")
// 协程作用域,用于异步操作
private val coroutineScope = CoroutineScope(Dispatchers.IO)
// Each module class must implement the definition function. The definition consists of components
// that describes the module's functionality and behavior.
@ -43,78 +61,195 @@ class MyNativeModule : Module() {
))
}
// 模拟初始化方法
// 初始化 Twilio Conversations 客户端
AsyncFunction("initialize") { token: String, promise: Promise ->
try {
// 模拟初始化延迟
Thread.sleep(1000)
// 模拟初始化成功
isInitialized = true
// 添加一些模拟对话
mockConversations.clear()
mockConversations.add(mapOf(
"sid" to "CH001",
"friendlyName" to "示例对话1"
))
mockConversations.add(mapOf(
"sid" to "CH002",
"friendlyName" to "示例对话2"
))
promise.resolve("客户端已成功初始化(模拟)")
} catch (e: Exception) {
// 忽略异常
}
}
// 模拟获取会话列表方法
AsyncFunction("getConversations") { promise: Promise ->
try {
if (!isInitialized) {
if (token.isEmpty()) {
promise.reject("INVALID_TOKEN", "Token cannot be empty", null)
return@AsyncFunction
}
// 模拟获取对话延迟
Thread.sleep(500)
// 使用 Twilio SDK 6.2.0 兼容的 API 创建客户端
ConversationsClient.create(context, token, object : CallbackListener<ConversationsClient>() {
override fun onSuccess(client: ConversationsClient) {
conversationsClient = client
isInitialized = true
// 返回模拟对话列表
promise.resolve(mockConversations)
// 注册客户端监听器
client.addListener(object : ConversationsClientListener {
override fun onClientSynchronization(status: ConversationsClient.SynchronizationStatus) {
if (status == ConversationsClient.SynchronizationStatus.COMPLETED) {
sendEvent("onClientSynchronized", mapOf("status" to "synchronized"))
}
}
override fun onError(errorInfo: ErrorInfo) {
sendEvent("onClientFailed", mapOf(
"errorCode" to errorInfo.code,
"errorMessage" to errorInfo.message
))
}
override fun onConversationAdded(conversation: Conversation) {
sendEvent("onConversationAdded", mapOf(
"sid" to conversation.sid,
"friendlyName" to (conversation.friendlyName ?: conversation.sid)
))
}
override fun onConversationUpdated(conversation: Conversation, reason: Conversation.UpdateReason) {
sendEvent("onConversationUpdated", mapOf(
"sid" to conversation.sid,
"friendlyName" to (conversation.friendlyName ?: conversation.sid)
))
}
override fun onConversationDeleted(conversation: Conversation) {
sendEvent("onConversationDeleted", mapOf("sid" to conversation.sid))
}
// 实现所有必需的接口方法
override fun onConversationSynchronizationChange(conversation: Conversation) {}
override fun onUserSubscribed(user: User) {}
override fun onUserUnsubscribed(user: User) {}
override fun onUserUpdated(user: User, reason: User.UpdateReason) {}
override fun onNewMessageNotification(conversationSid: String, messageSid: String, messageIndex: Long) {}
override fun onAddedToConversationNotification(conversationSid: String) {}
override fun onRemovedFromConversationNotification(conversationSid: String) {}
override fun onNotificationSubscribed() {}
override fun onNotificationFailed(errorInfo: ErrorInfo) {}
override fun onConnectionStateChange(state: ConversationsClient.ConnectionState) {}
override fun onTokenExpired() {}
override fun onTokenAboutToExpire() {}
})
promise.resolve("Twilio Conversations 客户端已成功初始化")
}
override fun onError(errorInfo: ErrorInfo) {
// 初始化失败
promise.reject("INITIALIZATION_FAILED", "初始化失败: ${errorInfo.message}", null)
}
})
} catch (e: Exception) {
// 忽略异常
promise.reject("INITIALIZATION_EXCEPTION", "初始化异常: ${e.message}", e)
}
}
// 模拟创建对话方法
// 获取所有对话列表
AsyncFunction("getConversations") { promise: Promise ->
try {
if (!isInitialized || conversationsClient == null) {
promise.reject("NOT_INITIALIZED", "Twilio Conversations 客户端未初始化", null)
return@AsyncFunction
}
// 使用 Twilio SDK 6.2.0 兼容的 API 获取对话列表
conversationsClient!!.getConversations(object : CallbackListener<List<Conversation>>() {
override fun onSuccess(conversationList: List<Conversation>) {
val conversations = mutableListOf<Map<String, Any>>()
// 遍历对话列表并转换为可序列化的格式
for (conversation in conversationList) {
val convData = mapOf(
"sid" to conversation.sid,
"friendlyName" to (conversation.friendlyName ?: conversation.sid),
"dateCreated" to conversation.dateCreated.toString(),
"dateUpdated" to conversation.dateUpdated.toString()
)
conversations.add(convData)
}
promise.resolve(conversations)
}
override fun onError(errorInfo: ErrorInfo) {
promise.reject("GET_CONVERSATIONS_FAILED", "获取对话列表失败: ${errorInfo.message}", null)
}
})
} catch (e: Exception) {
promise.reject("GET_CONVERSATIONS_EXCEPTION", "获取对话列表异常: ${e.message}", e)
}
}
// 创建对话方法
AsyncFunction("createConversation") { name: String, promise: Promise ->
try {
if (!isInitialized) {
if (!isInitialized || conversationsClient == null) {
promise.reject("NOT_INITIALIZED", "客户端未初始化", null)
return@AsyncFunction
}
if (name.trim().isEmpty()) {
promise.reject("INVALID_NAME", "对话名称不能为空", null)
return@AsyncFunction
}
// 模拟创建对话延迟
Thread.sleep(800)
// 使用 Twilio SDK 6.2.0 的兼容 API 创建对话
conversationsClient!!.createConversation(name, object : CallbackListener<Conversation>() {
override fun onSuccess(conversation: Conversation) {
promise.resolve(mapOf(
"status" to "success",
"friendlyName" to name,
"sid" to conversation.sid
))
}
// 生成随机SID
val newSid = "CH${System.currentTimeMillis()}"
override fun onError(errorInfo: ErrorInfo) {
promise.reject("CREATE_CONVERSATION_ERROR", "创建对话失败: ${errorInfo.message}", null)
}
})
} catch (e: Exception) {
promise.reject("CREATE_CONVERSATION_EXCEPTION", "创建对话异常: ${e.message}", e)
}
}
// 创建新对话
val newConversation = mapOf(
"sid" to newSid,
"friendlyName" to name
// 获取客户端连接状态
AsyncFunction("getClientStatus") { promise: Promise ->
try {
if (!isInitialized || conversationsClient == null) {
promise.reject("NOT_INITIALIZED", "Twilio Conversations 客户端未初始化", null)
return@AsyncFunction
}
// 使用 Twilio SDK 6.2.0 兼容的 API 获取连接状态
val connectionState = conversationsClient!!.connectionState
val status = when (connectionState) {
ConversationsClient.ConnectionState.CONNECTING -> "connecting"
ConversationsClient.ConnectionState.CONNECTED -> "connected"
ConversationsClient.ConnectionState.DISCONNECTING -> "disconnecting"
ConversationsClient.ConnectionState.DISCONNECTED -> "disconnected"
ConversationsClient.ConnectionState.ERROR -> "error"
else -> "unknown"
}
val result = mapOf(
"status" to status,
"connectionState" to connectionState.toString()
)
// 添加到模拟对话列表
mockConversations.add(newConversation)
promise.resolve(newConversation)
promise.resolve(result)
} catch (e: Exception) {
// 忽略异常
promise.reject("GET_CLIENT_STATUS_EXCEPTION", "获取客户端状态异常: ${e.message}", e)
}
}
// 断开客户端连接
AsyncFunction("disconnect") { promise: Promise ->
try {
if (!isInitialized || conversationsClient == null) {
promise.resolve("already_disconnected")
return@AsyncFunction
}
// 断开连接
conversationsClient!!.shutdown()
isInitialized = false
conversationsClient = null
promise.resolve("disconnected")
} catch (e: Exception) {
promise.reject("DISCONNECT_EXCEPTION", "断开连接异常: ${e.message}", e)
}
}

View File

@ -4,12 +4,6 @@
"modules": ["MyNativeModule"]
},
"android": {
"gradleAarProjects": [
{
"name": "twilio-conversations-android",
"aarFilePath": "android/libs/twilio-conversations-android-6.0.4.aar"
}
],
"modules": ["expo.modules.mynativemodule.MyNativeModule"]
}
}

View File

@ -1,30 +1,49 @@
import ExpoModulesCore
public class MyNativeModule: Module {
// Each module class must implement the definition function. The definition consists of components
// that describes the module's functionality and behavior.
// See https://docs.expo.dev/modules/module-api for more details about available components.
// Twilio Conversations SDK
import TwilioConversationsClient
public class MyNativeModule: Module, TwilioConversationsClientDelegate {
// Twilio
private var conversationsClient: TwilioConversationsClient?
// Promise
private var initCompletion: PromiseCompletionBlock?
private var createConversationCompletion: PromiseCompletionBlock?
// Each module class must implement the definition function.
public func definition() -> ModuleDefinition {
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
// The module will be accessible from `requireNativeModule('MyNativeModule')` in JavaScript.
//
Name("MyNativeModule")
// Defines constant property on the module.
//
Constant("PI") {
Double.pi
}
// Defines event names that the module can send to JavaScript.
Events("onChange")
// JavaScript
Events(
"onChange",
"onClientSynchronized",
"onClientError",
"onConnectionStateChanged",
"onConversationAdded",
"onConversationUpdated",
"onConversationDeleted",
"onMessageAdded",
"onMessageUpdated",
"onMessageDeleted",
"onParticipantAdded",
"onParticipantUpdated",
"onParticipantDeleted"
)
// Defines a JavaScript synchronous function that runs the native code on the JavaScript thread.
//
Function("hello") {
return "Hello world! 👋"
}
// Defines a JavaScript function that always returns a Promise and whose native code
// is by default dispatched on the different thread than the JavaScript runtime runs on.
//
AsyncFunction("setValueAsync") { (value: String) in
// Send an event to JavaScript.
self.sendEvent("onChange", [
@ -32,10 +51,155 @@ public class MyNativeModule: Module {
])
}
// Enables the module to be used as a native view. Definition components that are accepted as part of the
// view definition: Prop, Events.
// Twilio Conversations
AsyncFunction("initialize") { (token: String, completion: @escaping PromiseCompletionBlock) -> Void in
if token.isEmpty {
completion(["error": "Token cannot be empty"])
return
}
//
self.initCompletion = completion
// Twilio
TwilioConversationsClient.conversationsClient(withToken: token, properties: nil, delegate: self) {
result, error in
if let error = error {
self.sendEvent("onClientError", ["error": error.localizedDescription])
self.initCompletion?(nil, error)
return
}
if let client = result {
self.conversationsClient = client
//
self.initCompletion?("Twilio Conversations 客户端已成功初始化", nil)
} else {
self.initCompletion?(["error": "Failed to initialize client"])
}
}
}
//
AsyncFunction("getConversations") { (completion: @escaping PromiseCompletionBlock) -> Void in
guard let client = self.conversationsClient else {
completion(["error": "客户端未初始化"])
return
}
//
client.conversations() { result, error in
if let error = error {
completion(nil, error)
return
}
if let conversations = result {
// JS
let jsConversations = conversations.map { conversation -> [String: Any] in
var conversationDict: [String: Any] = [
"sid": conversation.sid,
"friendlyName": conversation.friendlyName ?? "未命名对话"
]
//
if let dateCreated = conversation.dateCreated {
conversationDict["dateCreated"] = Int(dateCreated.timeIntervalSince1970 * 1000)
}
if let dateUpdated = conversation.dateUpdated {
conversationDict["dateUpdated"] = Int(dateUpdated.timeIntervalSince1970 * 1000)
}
return conversationDict
}
completion(jsConversations, nil)
} else {
completion([], nil)
}
}
}
//
AsyncFunction("createConversation") { (friendlyName: String, completion: @escaping PromiseCompletionBlock) -> Void in
guard let client = self.conversationsClient else {
completion(["error": "客户端未初始化"])
return
}
if friendlyName.isEmpty {
completion(["error": "对话名称不能为空"])
return
}
//
self.createConversationCompletion = completion
//
client.createConversation(options: ["friendlyName": friendlyName]) { result, error in
if let error = error {
completion(nil, error)
return
}
if let conversation = result {
//
completion(["status": "creating", "friendlyName": conversation.friendlyName ?? friendlyName], nil)
//
self.sendEvent("onConversationAdded", [
"sid": conversation.sid,
"friendlyName": conversation.friendlyName ?? friendlyName
])
} else {
completion(["error": "创建对话失败"])
}
}
}
//
AsyncFunction("getClientStatus") { (completion: @escaping PromiseCompletionBlock) -> Void in
guard let client = self.conversationsClient else {
completion("not_initialized")
return
}
//
switch client.connectionState {
case .connecting:
completion("connecting")
case .connected:
completion("connected")
case .disconnected:
completion("disconnected")
case .denied:
completion("denied")
case .error:
completion("error")
@unknown default:
completion("unknown")
}
}
//
AsyncFunction("disconnect") { (completion: @escaping PromiseCompletionBlock) -> Void in
guard let client = self.conversationsClient else {
completion("already_disconnected")
return
}
//
client.shutdown()
self.conversationsClient = nil
completion("disconnected")
}
//
View(MyNativeModuleView.self) {
// Defines a setter for the `url` prop.
// urlsetter
Prop("url") { (view: MyNativeModuleView, url: URL) in
if view.webView.url != url {
view.webView.load(URLRequest(url: url))
@ -45,4 +209,121 @@ public class MyNativeModule: Module {
Events("onLoad")
}
}
// MARK: - TwilioConversationsClientDelegate
//
public func conversationsClientDidSynchronize(_ client: TwilioConversationsClient) {
sendEvent("onClientSynchronized", ["status": "synchronized"])
}
//
public func conversationsClient(_ client: TwilioConversationsClient, connectionStateChanged state: TwilioConversationsClient.ConnectionState) {
var stateString: String
switch state {
case .connecting:
stateString = "connecting"
case .connected:
stateString = "connected"
case .disconnected:
stateString = "disconnected"
case .denied:
stateString = "denied"
case .error:
stateString = "error"
@unknown default:
stateString = "unknown"
}
sendEvent("onConnectionStateChanged", ["state": stateString])
}
//
public func conversationsClient(_ client: TwilioConversationsClient, conversationAdded conversation: TCHConversation) {
sendEvent("onConversationAdded", [
"sid": conversation.sid,
"friendlyName": conversation.friendlyName ?? "未命名对话"
])
}
//
public func conversationsClient(_ client: TwilioConversationsClient, conversationUpdated conversation: TCHConversation) {
sendEvent("onConversationUpdated", [
"sid": conversation.sid,
"friendlyName": conversation.friendlyName ?? "未命名对话"
])
}
//
public func conversationsClient(_ client: TwilioConversationsClient, conversationDeleted conversation: TCHConversation) {
sendEvent("onConversationDeleted", ["sid": conversation.sid])
}
//
public func conversationsClient(_ client: TwilioConversationsClient, conversation: TCHConversation, messageAdded message: TCHMessage) {
sendEvent("onMessageAdded", [
"conversationSid": conversation.sid,
"messageSid": message.sid
])
}
//
public func conversationsClient(_ client: TwilioConversationsClient, conversation: TCHConversation, messageUpdated message: TCHMessage) {
sendEvent("onMessageUpdated", [
"conversationSid": conversation.sid,
"messageSid": message.sid
])
}
//
public func conversationsClient(_ client: TwilioConversationsClient, conversation: TCHConversation, messageDeleted message: TCHMessage) {
sendEvent("onMessageDeleted", [
"conversationSid": conversation.sid,
"messageSid": message.sid
])
}
//
public func conversationsClient(_ client: TwilioConversationsClient, conversation: TCHConversation, participantAdded participant: TCHParticipant) {
sendEvent("onParticipantAdded", [
"conversationSid": conversation.sid,
"participantIdentity": participant.identity ?? ""
])
}
//
public func conversationsClient(_ client: TwilioConversationsClient, conversation: TCHConversation, participantUpdated participant: TCHParticipant) {
sendEvent("onParticipantUpdated", [
"conversationSid": conversation.sid,
"participantIdentity": participant.identity ?? ""
])
}
//
public func conversationsClient(_ client: TwilioConversationsClient, conversation: TCHConversation, participantDeleted participant: TCHParticipant) {
sendEvent("onParticipantDeleted", [
"conversationSid": conversation.sid,
"participantIdentity": participant.identity ?? ""
])
}
//
public func conversationsClient(_ client: TwilioConversationsClient, synchronizationStatusChanged status: TCHClientSynchronizationStatus) {
//
var statusString: String
switch status {
case .started:
statusString = "started"
case .completed:
statusString = "completed"
case .failed:
statusString = "failed"
@unknown default:
statusString = "unknown"
}
sendEvent("onClientSynchronized", ["status": statusString])
}
}

View File

@ -1,6 +1,6 @@
import { NativeModule, requireNativeModule } from 'expo';
import { MyNativeModuleEvents, ConversationOptions, Conversation, TwilioConversationsOptions } from './MyNativeModule.types';
import { MyNativeModuleEvents, ConversationOptions, Conversation, TwilioConversationsOptions, CreateConversationResponse, ClientStatus } from './MyNativeModule.types';
declare class MyNativeModule extends NativeModule<MyNativeModuleEvents> {
PI: number;
@ -11,19 +11,34 @@ declare class MyNativeModule extends NativeModule<MyNativeModuleEvents> {
/**
* Twilio Conversations客户端
* @param token Twilio访问令牌
* @returns
*/
initialize(token: string): Promise<{ status: string }>;
initialize(token: string): Promise<string>;
/**
*
* @param friendlyName
* @returns
*/
createConversation(friendlyName: string): Promise<Conversation>;
createConversation(friendlyName: string): Promise<CreateConversationResponse>;
/**
*
* @returns
*/
getConversations(): Promise<Conversation[]>;
/**
*
* @returns
*/
getClientStatus(): Promise<ClientStatus>;
/**
*
* @returns
*/
disconnect(): Promise<string>;
}
// This call loads the native module object from the JSI.

View File

@ -4,8 +4,46 @@ export type OnLoadEventPayload = {
url: string;
};
// Twilio Conversations相关事件类型
export type MyNativeModuleEvents = {
onChange: (params: ChangeEventPayload) => void;
onClientSynchronized: (params: { status: string }) => void;
onClientFailed: (params: { errorCode: number; errorMessage: string }) => void;
onConversationAdded: (params: { sid: string; friendlyName: string }) => void;
onConversationUpdated: (params: { sid: string; friendlyName: string }) => void;
onConversationDeleted: (params: { sid: string }) => void;
onNotificationNewMessage: (params: {
conversationSid: string;
messageSid: string;
author: string;
body: string;
}) => void;
onNotificationAddedToConversation: (params: {
sid: string;
friendlyName: string;
}) => void;
onNotificationRemovedFromConversation: (params: {
sid: string;
friendlyName: string;
}) => void;
onNotificationInvitedToConversation: (params: {
sid: string;
friendlyName: string;
}) => void;
onNotificationParticipantJoined: (params: {
conversationSid: string;
participantSid: string;
identity: string;
}) => void;
onNotificationParticipantLeft: (params: {
conversationSid: string;
participantSid: string;
identity: string;
}) => void;
onUserUpdated: (params: {
identity: string;
friendlyName: string;
}) => void;
};
export type ChangeEventPayload = {
@ -26,8 +64,17 @@ export interface ConversationOptions {
export interface Conversation {
sid: string;
friendlyName: string;
dateCreated?: number;
dateUpdated?: number;
}
export interface CreateConversationResponse {
status: string;
friendlyName: string;
}
export type ClientStatus = 'connected' | 'connecting' | 'disconnected' | 'disconnecting' | 'not_initialized' | 'unknown';
export interface TwilioConversationsOptions {
// 未来可能的配置选项
}

View File

@ -1,19 +1,109 @@
import { registerWebModule, NativeModule } from 'expo';
import { ChangeEventPayload } from './MyNativeModule.types';
type MyNativeModuleEvents = {
onChange: (params: ChangeEventPayload) => void;
}
import { MyNativeModuleEvents, Conversation, CreateConversationResponse, ClientStatus } from './MyNativeModule.types';
class MyNativeModule extends NativeModule<MyNativeModuleEvents> {
PI = Math.PI;
private isInitialized = false;
private mockConversations: Conversation[] = [];
async setValueAsync(value: string): Promise<void> {
this.emit('onChange', { value });
}
hello() {
return 'Hello world! 👋';
}
// Twilio Conversations相关方法的Web模拟实现
async initialize(token: string): Promise<string> {
if (!token || token.trim() === '') {
throw new Error('Token cannot be empty');
}
// 模拟初始化延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟初始化成功
this.isInitialized = true;
// 添加一些模拟对话
this.mockConversations = [
{ sid: 'CH001', friendlyName: '示例对话1', dateCreated: Date.now() - 86400000, dateUpdated: Date.now() - 3600000 },
{ sid: 'CH002', friendlyName: '示例对话2', dateCreated: Date.now() - 172800000, dateUpdated: Date.now() - 7200000 }
];
// 模拟事件
setTimeout(() => {
this.emit('onClientSynchronized', { status: 'synchronized' });
}, 500);
return 'Twilio Conversations 客户端已成功初始化';
}
async createConversation(friendlyName: string): Promise<CreateConversationResponse> {
if (!this.isInitialized) {
throw new Error('客户端未初始化');
}
if (!friendlyName || friendlyName.trim() === '') {
throw new Error('对话名称不能为空');
}
// 模拟创建对话延迟
await new Promise(resolve => setTimeout(resolve, 800));
// 模拟创建对话
const newConversation: Conversation = {
sid: `CH${Date.now()}`,
friendlyName,
dateCreated: Date.now(),
dateUpdated: Date.now()
};
// 添加到模拟对话列表
this.mockConversations.push(newConversation);
// 模拟事件
setTimeout(() => {
this.emit('onConversationAdded', {
sid: newConversation.sid,
friendlyName: newConversation.friendlyName
});
}, 300);
return { status: 'creating', friendlyName };
}
async getConversations(): Promise<Conversation[]> {
if (!this.isInitialized) {
throw new Error('客户端未初始化');
}
// 模拟获取对话延迟
await new Promise(resolve => setTimeout(resolve, 500));
return [...this.mockConversations];
}
async getClientStatus(): Promise<ClientStatus> {
if (!this.isInitialized) {
return 'not_initialized';
}
return 'connected';
}
async disconnect(): Promise<string> {
if (!this.isInitialized) {
return 'already_disconnected';
}
this.isInitialized = false;
this.mockConversations = [];
return 'disconnected';
}
};
export default registerWebModule(MyNativeModule, 'MyNativeModule');

197
package-lock.json generated
View File

@ -13,7 +13,9 @@
"expo-status-bar": "~3.0.8",
"my-native-module": "file:./modules/my-native-module",
"react": "19.1.0",
"react-native": "0.81.5"
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-web": "^0.21.0"
},
"devDependencies": {
"@types/react": "~19.1.0",
@ -4197,6 +4199,15 @@
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cross-fetch": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz",
"integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.7.0"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -4220,6 +4231,15 @@
"node": ">=8"
}
},
"node_modules/css-in-js-utils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
"integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==",
"license": "MIT",
"dependencies": {
"hyphenate-style-name": "^1.0.3"
}
},
"node_modules/csstype": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.2.tgz",
@ -5103,6 +5123,36 @@
"bser": "2.1.1"
}
},
"node_modules/fbjs": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz",
"integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==",
"license": "MIT",
"dependencies": {
"cross-fetch": "^3.1.5",
"fbjs-css-vars": "^1.0.0",
"loose-envify": "^1.0.0",
"object-assign": "^4.1.0",
"promise": "^7.1.1",
"setimmediate": "^1.0.5",
"ua-parser-js": "^1.0.35"
}
},
"node_modules/fbjs-css-vars": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz",
"integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==",
"license": "MIT"
},
"node_modules/fbjs/node_modules/promise": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
"license": "MIT",
"dependencies": {
"asap": "~2.0.3"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@ -5402,6 +5452,12 @@
"node": ">= 14"
}
},
"node_modules/hyphenate-style-name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
"integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==",
"license": "BSD-3-Clause"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@ -5478,6 +5534,15 @@
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/inline-style-prefixer": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz",
"integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==",
"license": "MIT",
"dependencies": {
"css-in-js-utils": "^3.1.0"
}
},
"node_modules/invariant": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
@ -6973,6 +7038,26 @@
"integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-forge": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz",
@ -7352,6 +7437,12 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@ -7499,6 +7590,18 @@
"ws": "^7"
}
},
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.1.0"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@ -7572,6 +7675,38 @@
"react-native": "*"
}
},
"node_modules/react-native-web": {
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.21.2.tgz",
"integrity": "sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.6",
"@react-native/normalize-colors": "^0.74.1",
"fbjs": "^3.0.4",
"inline-style-prefixer": "^7.0.1",
"memoize-one": "^6.0.0",
"nullthrows": "^1.1.1",
"postcss-value-parser": "^4.2.0",
"styleq": "^0.1.3"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/react-native-web/node_modules/@react-native/normalize-colors": {
"version": "0.74.89",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz",
"integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==",
"license": "MIT"
},
"node_modules/react-native-web/node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",
@ -8035,6 +8170,12 @@
"node": ">= 0.8"
}
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@ -8326,6 +8467,12 @@
"integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==",
"license": "MIT"
},
"node_modules/styleq": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz",
"integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==",
"license": "MIT"
},
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
@ -8600,6 +8747,12 @@
"node": ">=0.6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@ -8638,6 +8791,32 @@
"node": ">=14.17"
}
},
"node_modules/ua-parser-js": {
"version": "1.0.41",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz",
"integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
},
{
"type": "github",
"url": "https://github.com/sponsors/faisalman"
}
],
"license": "MIT",
"bin": {
"ua-parser-js": "script/cli.js"
},
"engines": {
"node": "*"
}
},
"node_modules/undici": {
"version": "6.22.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz",
@ -8819,6 +8998,16 @@
"integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==",
"license": "MIT"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/whatwg-url-without-unicode": {
"version": "8.0.0-3",
"resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz",
@ -8833,6 +9022,12 @@
"node": ">=10"
}
},
"node_modules/whatwg-url/node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -14,7 +14,9 @@
"expo-status-bar": "~3.0.8",
"my-native-module": "file:./modules/my-native-module",
"react": "19.1.0",
"react-native": "0.81.5"
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-web": "^0.21.0"
},
"devDependencies": {
"@types/react": "~19.1.0",