chore: initial commit
这个提交包含在:
当前提交
ad0a31bd91
10
.gitignore
vendored
普通文件
10
.gitignore
vendored
普通文件
@ -0,0 +1,10 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
*.class
|
||||
target/
|
||||
build/
|
||||
.gradle/
|
||||
*.iml
|
||||
.idea/
|
||||
*.log
|
||||
26
package.json
普通文件
26
package.json
普通文件
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@xuqm/rn-sdk",
|
||||
"version": "0.1.0",
|
||||
"description": "XuqmGroup React Native SDK — IM, Push, Version Management",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"registry": "https://nexus.xuqinmin.com/repository/npm-hosted/"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "jest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-native": ">=0.76.0",
|
||||
"@react-native-async-storage/async-storage": ">=1.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.9.3",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-native": "^0.73.0"
|
||||
},
|
||||
"dependencies": {}
|
||||
}
|
||||
19
src/core/config.ts
普通文件
19
src/core/config.ts
普通文件
@ -0,0 +1,19 @@
|
||||
export interface XuqmSDKConfig {
|
||||
appId: string
|
||||
appKey: string
|
||||
appSecret: string
|
||||
apiBaseUrl: string
|
||||
imWsUrl: string
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
let _config: XuqmSDKConfig | null = null
|
||||
|
||||
export function initSDK(config: XuqmSDKConfig) {
|
||||
_config = config
|
||||
}
|
||||
|
||||
export function getConfig(): XuqmSDKConfig {
|
||||
if (!_config) throw new Error('XuqmSDK not initialized. Call initSDK() first.')
|
||||
return _config
|
||||
}
|
||||
48
src/core/http.ts
普通文件
48
src/core/http.ts
普通文件
@ -0,0 +1,48 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { getConfig } from './config'
|
||||
|
||||
const TOKEN_KEY = '@xuqm_sdk_token'
|
||||
|
||||
export async function getToken(): Promise<string | null> {
|
||||
return AsyncStorage.getItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
export async function saveToken(token: string): Promise<void> {
|
||||
return AsyncStorage.setItem(TOKEN_KEY, token)
|
||||
}
|
||||
|
||||
export async function clearToken(): Promise<void> {
|
||||
return AsyncStorage.removeItem(TOKEN_KEY)
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
options: { method?: string; body?: unknown; params?: Record<string, string> } = {},
|
||||
): Promise<T> {
|
||||
const config = getConfig()
|
||||
const token = await getToken()
|
||||
|
||||
let url = config.apiBaseUrl.replace(/\/$/, '') + path
|
||||
if (options.params) {
|
||||
const qs = new URLSearchParams(options.params).toString()
|
||||
url += (url.includes('?') ? '&' : '?') + qs
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: options.method ?? 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({ message: res.statusText }))
|
||||
throw new Error(err.message ?? `HTTP ${res.status}`)
|
||||
}
|
||||
|
||||
const json = await res.json()
|
||||
return json.data ?? json
|
||||
}
|
||||
86
src/im/imClient.ts
普通文件
86
src/im/imClient.ts
普通文件
@ -0,0 +1,86 @@
|
||||
import type { ChatType, ImMessage, ImEventListener, MsgType } from './types'
|
||||
|
||||
export class ImClient {
|
||||
private ws: WebSocket | null = null
|
||||
private listeners: ImEventListener[] = []
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||
private reconnectDelay = 3000
|
||||
private shouldReconnect = true
|
||||
|
||||
constructor(
|
||||
private readonly wsUrl: string,
|
||||
private readonly token: string,
|
||||
private readonly appId: string,
|
||||
) {}
|
||||
|
||||
connect() {
|
||||
this.shouldReconnect = true
|
||||
this._connect()
|
||||
}
|
||||
|
||||
private _connect() {
|
||||
const url = `${this.wsUrl}?token=${this.token}`
|
||||
this.ws = new WebSocket(url)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.reconnectDelay = 3000
|
||||
this.listeners.forEach(l => l.onConnected?.())
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg: ImMessage = JSON.parse(event.data)
|
||||
if (msg.chatType === 'GROUP') {
|
||||
this.listeners.forEach(l => l.onGroupMessage?.(msg))
|
||||
} else {
|
||||
this.listeners.forEach(l => l.onMessage?.(msg))
|
||||
}
|
||||
} catch (e) {
|
||||
this.listeners.forEach(l => l.onError?.('Parse error'))
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
this.listeners.forEach(l => l.onDisconnected?.(event.reason))
|
||||
if (this.shouldReconnect) {
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000)
|
||||
this._connect()
|
||||
}, this.reconnectDelay)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onerror = () => {
|
||||
this.listeners.forEach(l => l.onError?.('WebSocket error'))
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(toId: string, chatType: ChatType, msgType: MsgType, content: string, mentionedUserIds?: string) {
|
||||
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||
throw new Error('IM not connected')
|
||||
}
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'chat.send',
|
||||
data: { appId: this.appId, toId, chatType, msgType, content, mentionedUserIds },
|
||||
}))
|
||||
}
|
||||
|
||||
addListener(listener: ImEventListener) {
|
||||
this.listeners.push(listener)
|
||||
}
|
||||
|
||||
removeListener(listener: ImEventListener) {
|
||||
this.listeners = this.listeners.filter(l => l !== listener)
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.shouldReconnect = false
|
||||
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
|
||||
this.ws?.close(1000, 'User disconnect')
|
||||
this.ws = null
|
||||
}
|
||||
|
||||
isConnected(): boolean {
|
||||
return this.ws?.readyState === WebSocket.OPEN
|
||||
}
|
||||
}
|
||||
46
src/im/imSDK.ts
普通文件
46
src/im/imSDK.ts
普通文件
@ -0,0 +1,46 @@
|
||||
import { apiRequest, saveToken } from '../core/http'
|
||||
import { getConfig } from '../core/config'
|
||||
import { ImClient } from './imClient'
|
||||
import type { ChatType, ImEventListener, MsgType } from './types'
|
||||
|
||||
let client: ImClient | null = null
|
||||
|
||||
export const ImSDK = {
|
||||
async login(userId: string, nickname?: string, avatar?: string): Promise<void> {
|
||||
const config = getConfig()
|
||||
const res = await apiRequest<{ token: string }>('/api/im/auth/login', {
|
||||
method: 'POST',
|
||||
params: {
|
||||
appId: config.appId,
|
||||
userId,
|
||||
...(nickname ? { nickname } : {}),
|
||||
...(avatar ? { avatar } : {}),
|
||||
},
|
||||
})
|
||||
await saveToken(res.token)
|
||||
client = new ImClient(config.imWsUrl, res.token, config.appId)
|
||||
client.connect()
|
||||
},
|
||||
|
||||
sendMessage(toId: string, chatType: ChatType, msgType: MsgType, content: string, mentionedUserIds?: string) {
|
||||
if (!client) throw new Error('ImSDK: not logged in')
|
||||
client.sendMessage(toId, chatType, msgType, content, mentionedUserIds)
|
||||
},
|
||||
|
||||
addListener(listener: ImEventListener) {
|
||||
client?.addListener(listener)
|
||||
},
|
||||
|
||||
removeListener(listener: ImEventListener) {
|
||||
client?.removeListener(listener)
|
||||
},
|
||||
|
||||
disconnect() {
|
||||
client?.disconnect()
|
||||
client = null
|
||||
},
|
||||
|
||||
isConnected() {
|
||||
return client?.isConnected() ?? false
|
||||
},
|
||||
}
|
||||
36
src/im/types.ts
普通文件
36
src/im/types.ts
普通文件
@ -0,0 +1,36 @@
|
||||
export type ChatType = 'SINGLE' | 'GROUP'
|
||||
|
||||
export type MsgType =
|
||||
| 'TEXT'
|
||||
| 'IMAGE'
|
||||
| 'VIDEO'
|
||||
| 'AUDIO'
|
||||
| 'FILE'
|
||||
| 'CUSTOM'
|
||||
| 'LOCATION'
|
||||
| 'NOTIFY'
|
||||
| 'REVOKED'
|
||||
| 'FORWARD'
|
||||
|
||||
export type MsgStatus = 'SENT' | 'DELIVERED' | 'READ' | 'REVOKED'
|
||||
|
||||
export interface ImMessage {
|
||||
id: string
|
||||
appId: string
|
||||
fromUserId: string
|
||||
toId: string
|
||||
chatType: ChatType
|
||||
msgType: MsgType
|
||||
content: string
|
||||
status: MsgStatus
|
||||
mentionedUserIds?: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface ImEventListener {
|
||||
onConnected?: () => void
|
||||
onDisconnected?: (reason?: string) => void
|
||||
onMessage?: (msg: ImMessage) => void
|
||||
onGroupMessage?: (msg: ImMessage) => void
|
||||
onError?: (error: string) => void
|
||||
}
|
||||
13
src/index.ts
普通文件
13
src/index.ts
普通文件
@ -0,0 +1,13 @@
|
||||
export { initSDK, getConfig } from './core/config'
|
||||
export type { XuqmSDKConfig } from './core/config'
|
||||
export { getToken, saveToken, clearToken } from './core/http'
|
||||
|
||||
export { ImSDK } from './im/imSDK'
|
||||
export { ImClient } from './im/imClient'
|
||||
export type { ImMessage, ChatType, MsgType, MsgStatus, ImEventListener } from './im/types'
|
||||
|
||||
export { PushSDK } from './push/pushSDK'
|
||||
export type { PushVendor } from './push/pushSDK'
|
||||
|
||||
export { UpdateSDK } from './update/updateSDK'
|
||||
export type { AppUpdateInfo, RnUpdateInfo } from './update/updateSDK'
|
||||
28
src/push/pushSDK.ts
普通文件
28
src/push/pushSDK.ts
普通文件
@ -0,0 +1,28 @@
|
||||
import { Platform } from 'react-native'
|
||||
import { apiRequest } from '../core/http'
|
||||
import { getConfig } from '../core/config'
|
||||
|
||||
export type PushVendor = 'HUAWEI' | 'XIAOMI' | 'OPPO' | 'VIVO' | 'HONOR' | 'APNS' | 'FCM'
|
||||
|
||||
export const PushSDK = {
|
||||
async registerToken(userId: string, vendor: PushVendor, token: string): Promise<void> {
|
||||
const config = getConfig()
|
||||
await apiRequest('/api/push/register', {
|
||||
method: 'POST',
|
||||
params: {
|
||||
appId: config.appId,
|
||||
userId,
|
||||
vendor,
|
||||
token,
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async unregisterToken(userId: string): Promise<void> {
|
||||
const config = getConfig()
|
||||
await apiRequest('/api/push/unregister', {
|
||||
method: 'DELETE',
|
||||
params: { appId: config.appId, userId },
|
||||
})
|
||||
},
|
||||
}
|
||||
53
src/update/updateSDK.ts
普通文件
53
src/update/updateSDK.ts
普通文件
@ -0,0 +1,53 @@
|
||||
import { Platform, Linking } from 'react-native'
|
||||
import { apiRequest } from '../core/http'
|
||||
import { getConfig } from '../core/config'
|
||||
|
||||
export interface AppUpdateInfo {
|
||||
needsUpdate: boolean
|
||||
versionName?: string
|
||||
versionCode?: number
|
||||
downloadUrl?: string
|
||||
changeLog?: string
|
||||
forceUpdate?: boolean
|
||||
appStoreUrl?: string
|
||||
marketUrl?: string
|
||||
}
|
||||
|
||||
export interface RnUpdateInfo {
|
||||
needsUpdate: boolean
|
||||
latestVersion: string
|
||||
downloadUrl: string
|
||||
md5: string
|
||||
minCommonVersion: string
|
||||
note: string
|
||||
}
|
||||
|
||||
export const UpdateSDK = {
|
||||
async checkAppUpdate(currentVersionCode: number): Promise<AppUpdateInfo> {
|
||||
const config = getConfig()
|
||||
return apiRequest<AppUpdateInfo>('/api/v1/updates/app/check', {
|
||||
params: {
|
||||
appId: config.appId,
|
||||
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
|
||||
currentVersionCode: String(currentVersionCode),
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async openAppStore(appStoreUrl?: string, marketUrl?: string): Promise<void> {
|
||||
const url = Platform.OS === 'ios' ? appStoreUrl : marketUrl
|
||||
if (url) await Linking.openURL(url)
|
||||
},
|
||||
|
||||
async checkRnUpdate(moduleId: string, currentVersion: string): Promise<RnUpdateInfo> {
|
||||
const config = getConfig()
|
||||
return apiRequest<RnUpdateInfo>('/api/v1/rn/update/check', {
|
||||
params: {
|
||||
appId: config.appId,
|
||||
moduleId,
|
||||
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
|
||||
currentVersion,
|
||||
},
|
||||
})
|
||||
},
|
||||
}
|
||||
15
tsconfig.json
普通文件
15
tsconfig.json
普通文件
@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"jsx": "react-native",
|
||||
"lib": ["ES2020"],
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
正在加载...
在新工单中引用
屏蔽一个用户