feat: add rn sdk chat and update support
这个提交包含在:
父节点
61ffd1bc8c
当前提交
b747901cfe
2968
package-lock.json
自动生成的
普通文件
2968
package-lock.json
自动生成的
普通文件
文件差异内容过多而无法显示
加载差异
@ -3,6 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "XuqmGroup React Native SDK — IM, Push, Version Management",
|
"description": "XuqmGroup React Native SDK — IM, Push, Version Management",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
"react-native": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
"private": false,
|
"private": false,
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
26
src/core/sdk.ts
普通文件
26
src/core/sdk.ts
普通文件
@ -0,0 +1,26 @@
|
|||||||
|
import { getConfig, initSDK, type XuqmSDKConfig } from './config'
|
||||||
|
import { clearToken, getToken, saveToken } from './http'
|
||||||
|
|
||||||
|
export const XuqmSDK = {
|
||||||
|
init(config: XuqmSDKConfig) {
|
||||||
|
initSDK(config)
|
||||||
|
},
|
||||||
|
|
||||||
|
getConfig,
|
||||||
|
|
||||||
|
async getToken() {
|
||||||
|
return getToken()
|
||||||
|
},
|
||||||
|
|
||||||
|
async setToken(token: string | null) {
|
||||||
|
if (token) {
|
||||||
|
await saveToken(token)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await clearToken()
|
||||||
|
},
|
||||||
|
|
||||||
|
async clearToken() {
|
||||||
|
await clearToken()
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -1,4 +1,10 @@
|
|||||||
import type { ChatType, ImMessage, ImEventListener, MsgType } from './types'
|
import type { ImEventListener, ImMessage, SendMessageParams } from './types'
|
||||||
|
|
||||||
|
interface StompFrame {
|
||||||
|
command: string
|
||||||
|
headers: Record<string, string>
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
export class ImClient {
|
export class ImClient {
|
||||||
private ws: WebSocket | null = null
|
private ws: WebSocket | null = null
|
||||||
@ -6,6 +12,8 @@ export class ImClient {
|
|||||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
private reconnectDelay = 3000
|
private reconnectDelay = 3000
|
||||||
private shouldReconnect = true
|
private shouldReconnect = true
|
||||||
|
private readonly subscriptionId = 'sub-user-queue'
|
||||||
|
private groupSubscriptions = new Set<string>()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly wsUrl: string,
|
private readonly wsUrl: string,
|
||||||
@ -15,54 +23,70 @@ export class ImClient {
|
|||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
this.shouldReconnect = true
|
this.shouldReconnect = true
|
||||||
this._connect()
|
this.openSocket()
|
||||||
}
|
}
|
||||||
|
|
||||||
private _connect() {
|
sendMessage(
|
||||||
const url = `${this.wsUrl}?token=${this.token}`
|
toId: string,
|
||||||
this.ws = new WebSocket(url)
|
chatType: SendMessageParams['chatType'],
|
||||||
|
msgType: SendMessageParams['msgType'],
|
||||||
this.ws.onopen = () => {
|
content: string,
|
||||||
this.reconnectDelay = 3000
|
mentionedUserIds?: string,
|
||||||
this.listeners.forEach(l => l.onConnected?.())
|
) {
|
||||||
|
this.send({
|
||||||
|
toId,
|
||||||
|
chatType,
|
||||||
|
msgType,
|
||||||
|
content,
|
||||||
|
mentionedUserIds,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ws.onmessage = (event) => {
|
send(params: SendMessageParams) {
|
||||||
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) {
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||||
throw new Error('IM not connected')
|
throw new Error('IM not connected')
|
||||||
}
|
}
|
||||||
this.ws.send(JSON.stringify({
|
|
||||||
type: 'chat.send',
|
this.sendFrame(
|
||||||
data: { appId: this.appId, toId, chatType, msgType, content, mentionedUserIds },
|
'SEND',
|
||||||
}))
|
{
|
||||||
|
destination: '/app/chat.send',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
JSON.stringify({
|
||||||
|
appId: this.appId,
|
||||||
|
toId: params.toId,
|
||||||
|
chatType: params.chatType,
|
||||||
|
msgType: params.msgType,
|
||||||
|
content: params.content,
|
||||||
|
mentionedUserIds: params.mentionedUserIds ?? '',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
revoke(messageId: string) {
|
||||||
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
||||||
|
throw new Error('IM not connected')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sendFrame(
|
||||||
|
'SEND',
|
||||||
|
{
|
||||||
|
destination: '/app/chat.revoke',
|
||||||
|
'content-type': 'application/json',
|
||||||
|
},
|
||||||
|
JSON.stringify({
|
||||||
|
appId: this.appId,
|
||||||
|
messageId,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeGroup(groupId: string) {
|
||||||
|
this.groupSubscriptions.add(groupId)
|
||||||
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||||
|
this.subscribe(`/topic/group/${groupId}`, `group-${groupId}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addListener(listener: ImEventListener) {
|
addListener(listener: ImEventListener) {
|
||||||
@ -70,12 +94,15 @@ export class ImClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeListener(listener: ImEventListener) {
|
removeListener(listener: ImEventListener) {
|
||||||
this.listeners = this.listeners.filter(l => l !== listener)
|
this.listeners = this.listeners.filter(item => item !== listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
this.shouldReconnect = false
|
this.shouldReconnect = false
|
||||||
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer)
|
||||||
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||||
|
this.sendFrame('DISCONNECT')
|
||||||
|
}
|
||||||
this.ws?.close(1000, 'User disconnect')
|
this.ws?.close(1000, 'User disconnect')
|
||||||
this.ws = null
|
this.ws = null
|
||||||
}
|
}
|
||||||
@ -83,4 +110,100 @@ export class ImClient {
|
|||||||
isConnected(): boolean {
|
isConnected(): boolean {
|
||||||
return this.ws?.readyState === WebSocket.OPEN
|
return this.ws?.readyState === WebSocket.OPEN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openSocket() {
|
||||||
|
this.ws = new WebSocket(this.wsUrl)
|
||||||
|
|
||||||
|
this.ws.onopen = () => {
|
||||||
|
this.sendFrame('CONNECT', {
|
||||||
|
'accept-version': '1.2',
|
||||||
|
Authorization: `Bearer ${this.token}`,
|
||||||
|
'heart-beat': '10000,10000',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onmessage = event => {
|
||||||
|
try {
|
||||||
|
const frames = this.parseFrames(String(event.data))
|
||||||
|
frames.forEach(frame => this.handleFrame(frame))
|
||||||
|
} catch {
|
||||||
|
this.listeners.forEach(listener => listener.onError?.('Parse error'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onclose = event => {
|
||||||
|
this.listeners.forEach(listener => listener.onDisconnected?.(event.reason))
|
||||||
|
if (this.shouldReconnect) {
|
||||||
|
this.reconnectTimer = setTimeout(() => {
|
||||||
|
this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30000)
|
||||||
|
this.openSocket()
|
||||||
|
}, this.reconnectDelay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws.onerror = () => {
|
||||||
|
this.listeners.forEach(listener => listener.onError?.('WebSocket error'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleFrame(frame: StompFrame) {
|
||||||
|
if (frame.command === 'CONNECTED') {
|
||||||
|
this.reconnectDelay = 3000
|
||||||
|
this.subscribe('/user/queue/messages', this.subscriptionId)
|
||||||
|
this.groupSubscriptions.forEach(groupId => {
|
||||||
|
this.subscribe(`/topic/group/${groupId}`, `group-${groupId}`)
|
||||||
|
})
|
||||||
|
this.listeners.forEach(listener => listener.onConnected?.())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.command === 'MESSAGE') {
|
||||||
|
const message: ImMessage = JSON.parse(frame.body)
|
||||||
|
if (message.chatType === 'GROUP') {
|
||||||
|
this.listeners.forEach(listener => listener.onGroupMessage?.(message))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.listeners.forEach(listener => listener.onMessage?.(message))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.command === 'ERROR') {
|
||||||
|
this.listeners.forEach(listener => listener.onError?.(frame.body || 'WebSocket error'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private subscribe(destination: string, id: string) {
|
||||||
|
this.sendFrame('SUBSCRIBE', { destination, id })
|
||||||
|
}
|
||||||
|
|
||||||
|
private sendFrame(command: string, headers: Record<string, string> = {}, body = '') {
|
||||||
|
if (!this.ws) return
|
||||||
|
const headerLines = Object.entries(headers)
|
||||||
|
.map(([key, value]) => `${key}:${value}`)
|
||||||
|
.join('\n')
|
||||||
|
const frame = `${command}\n${headerLines}\n\n${body}\u0000`
|
||||||
|
this.ws.send(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseFrames(raw: string): StompFrame[] {
|
||||||
|
return raw
|
||||||
|
.split('\u0000')
|
||||||
|
.map(frame => frame.replace(/^\n+/, '').trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map(frame => {
|
||||||
|
const separatorIndex = frame.indexOf('\n\n')
|
||||||
|
const headerBlock = separatorIndex >= 0 ? frame.slice(0, separatorIndex) : frame
|
||||||
|
const body = separatorIndex >= 0 ? frame.slice(separatorIndex + 2) : ''
|
||||||
|
const [command, ...headerLines] = headerBlock.split('\n').filter(Boolean)
|
||||||
|
const headers = Object.fromEntries(
|
||||||
|
headerLines
|
||||||
|
.filter(line => line.includes(':'))
|
||||||
|
.map(line => {
|
||||||
|
const index = line.indexOf(':')
|
||||||
|
return [line.slice(0, index), line.slice(index + 1)]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return { command, headers, body }
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { apiRequest, saveToken } from '../core/http'
|
|
||||||
import { getConfig } from '../core/config'
|
import { getConfig } from '../core/config'
|
||||||
|
import { apiRequest, getToken, saveToken } from '../core/http'
|
||||||
import { ImClient } from './imClient'
|
import { ImClient } from './imClient'
|
||||||
import type { ChatType, ImEventListener, MsgType } from './types'
|
import type { ChatType, ImEventListener, ImMessage, MsgType } from './types'
|
||||||
|
|
||||||
let client: ImClient | null = null
|
let client: ImClient | null = null
|
||||||
|
|
||||||
@ -22,9 +22,53 @@ export const ImSDK = {
|
|||||||
client.connect()
|
client.connect()
|
||||||
},
|
},
|
||||||
|
|
||||||
sendMessage(toId: string, chatType: ChatType, msgType: MsgType, content: string, mentionedUserIds?: string) {
|
async reconnect(): Promise<void> {
|
||||||
if (!client) throw new Error('ImSDK: not logged in')
|
const config = getConfig()
|
||||||
client.sendMessage(toId, chatType, msgType, content, mentionedUserIds)
|
const token = await getToken()
|
||||||
|
if (!token) throw new Error('ImSDK: token not found')
|
||||||
|
client = new ImClient(config.imWsUrl, token, config.appId)
|
||||||
|
client.connect()
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchHistory(toId: string, page = 0, size = 20): Promise<ImMessage[]> {
|
||||||
|
const config = getConfig()
|
||||||
|
const res = await apiRequest<{ content?: ImMessage[] } | ImMessage[]>(`/api/im/messages/history/${encodeURIComponent(toId)}`, {
|
||||||
|
params: {
|
||||||
|
appId: config.appId,
|
||||||
|
page: String(page),
|
||||||
|
size: String(size),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return Array.isArray(res) ? res : (res.content ?? [])
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendMessage(
|
||||||
|
toId: string,
|
||||||
|
chatType: ChatType,
|
||||||
|
msgType: MsgType,
|
||||||
|
content: string,
|
||||||
|
mentionedUserIds?: string,
|
||||||
|
): Promise<ImMessage> {
|
||||||
|
const config = getConfig()
|
||||||
|
return apiRequest<ImMessage>('/api/im/messages/send', {
|
||||||
|
method: 'POST',
|
||||||
|
params: { appId: config.appId },
|
||||||
|
body: {
|
||||||
|
toId,
|
||||||
|
chatType,
|
||||||
|
msgType,
|
||||||
|
content,
|
||||||
|
mentionedUserIds: mentionedUserIds ?? '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async revokeMessage(messageId: string): Promise<ImMessage> {
|
||||||
|
const config = getConfig()
|
||||||
|
return apiRequest<ImMessage>(`/api/im/messages/${encodeURIComponent(messageId)}/revoke`, {
|
||||||
|
method: 'POST',
|
||||||
|
params: { appId: config.appId },
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
addListener(listener: ImEventListener) {
|
addListener(listener: ImEventListener) {
|
||||||
@ -40,6 +84,10 @@ export const ImSDK = {
|
|||||||
client = null
|
client = null
|
||||||
},
|
},
|
||||||
|
|
||||||
|
subscribeGroup(groupId: string) {
|
||||||
|
client?.subscribeGroup(groupId)
|
||||||
|
},
|
||||||
|
|
||||||
isConnected() {
|
isConnected() {
|
||||||
return client?.isConnected() ?? false
|
return client?.isConnected() ?? false
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,6 +9,9 @@ export type MsgType =
|
|||||||
| 'CUSTOM'
|
| 'CUSTOM'
|
||||||
| 'LOCATION'
|
| 'LOCATION'
|
||||||
| 'NOTIFY'
|
| 'NOTIFY'
|
||||||
|
| 'RICH_TEXT'
|
||||||
|
| 'CALL_AUDIO'
|
||||||
|
| 'CALL_VIDEO'
|
||||||
| 'REVOKED'
|
| 'REVOKED'
|
||||||
| 'FORWARD'
|
| 'FORWARD'
|
||||||
|
|
||||||
@ -34,3 +37,11 @@ export interface ImEventListener {
|
|||||||
onGroupMessage?: (msg: ImMessage) => void
|
onGroupMessage?: (msg: ImMessage) => void
|
||||||
onError?: (error: string) => void
|
onError?: (error: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SendMessageParams {
|
||||||
|
toId: string
|
||||||
|
chatType: ChatType
|
||||||
|
msgType: MsgType
|
||||||
|
content: string
|
||||||
|
mentionedUserIds?: string
|
||||||
|
}
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
export { initSDK, getConfig } from './core/config'
|
export { initSDK, getConfig } from './core/config'
|
||||||
export type { XuqmSDKConfig } from './core/config'
|
export type { XuqmSDKConfig } from './core/config'
|
||||||
export { getToken, saveToken, clearToken } from './core/http'
|
export { getToken, saveToken, clearToken } from './core/http'
|
||||||
|
export { XuqmSDK } from './core/sdk'
|
||||||
|
|
||||||
export { ImSDK } from './im/imSDK'
|
export { ImSDK } from './im/imSDK'
|
||||||
export { ImClient } from './im/imClient'
|
export { ImClient } from './im/imClient'
|
||||||
export type { ImMessage, ChatType, MsgType, MsgStatus, ImEventListener } from './im/types'
|
export type { ImMessage, ChatType, MsgType, MsgStatus, ImEventListener, SendMessageParams } from './im/types'
|
||||||
|
|
||||||
export { PushSDK } from './push/pushSDK'
|
export { PushSDK } from './push/pushSDK'
|
||||||
export type { PushVendor } from './push/pushSDK'
|
export type { PushVendor } from './push/pushSDK'
|
||||||
|
|
||||||
export { UpdateSDK } from './update/updateSDK'
|
export { UpdateSDK } from './update/updateSDK'
|
||||||
export type { AppUpdateInfo, RnUpdateInfo } from './update/updateSDK'
|
export type { AppUpdateInfo, RnUpdateInfo, CachedRnBundle } from './update/updateSDK'
|
||||||
|
|||||||
9
src/types/async-storage.d.ts
vendored
普通文件
9
src/types/async-storage.d.ts
vendored
普通文件
@ -0,0 +1,9 @@
|
|||||||
|
declare module '@react-native-async-storage/async-storage' {
|
||||||
|
const AsyncStorage: {
|
||||||
|
getItem(key: string): Promise<string | null>
|
||||||
|
setItem(key: string, value: string): Promise<void>
|
||||||
|
removeItem(key: string): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AsyncStorage
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { Platform, Linking } from 'react-native'
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||||
import { apiRequest } from '../core/http'
|
import { Linking, Platform } from 'react-native'
|
||||||
import { getConfig } from '../core/config'
|
import { getConfig } from '../core/config'
|
||||||
|
import { apiRequest } from '../core/http'
|
||||||
|
|
||||||
export interface AppUpdateInfo {
|
export interface AppUpdateInfo {
|
||||||
needsUpdate: boolean
|
needsUpdate: boolean
|
||||||
@ -22,16 +23,54 @@ export interface RnUpdateInfo {
|
|||||||
note: string
|
note: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CachedRnBundle {
|
||||||
|
moduleId: string
|
||||||
|
version: string
|
||||||
|
md5: string
|
||||||
|
downloadedAt: string
|
||||||
|
source: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBundleCacheKey(moduleId: string) {
|
||||||
|
return `@xuqm_sdk_rn_bundle:${moduleId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDownloadUrl(rawUrl?: string) {
|
||||||
|
if (!rawUrl) return rawUrl
|
||||||
|
|
||||||
|
// Compatible with legacy server config where update base URL already includes /api/v1/updates.
|
||||||
|
if (rawUrl.includes('/api/v1/updates/api/v1/rn/files/')) {
|
||||||
|
return rawUrl.replace('/api/v1/updates/api/v1/rn/files/', '/api/v1/rn/files/')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawUrl.includes('/files/apk/')) {
|
||||||
|
try {
|
||||||
|
const url = new URL(rawUrl)
|
||||||
|
if (url.pathname.startsWith('/files/apk/')) {
|
||||||
|
return `${url.origin}/api/v1/updates${url.pathname}${url.search}`
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return rawUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawUrl
|
||||||
|
}
|
||||||
|
|
||||||
export const UpdateSDK = {
|
export const UpdateSDK = {
|
||||||
async checkAppUpdate(currentVersionCode: number): Promise<AppUpdateInfo> {
|
async checkAppUpdate(currentVersionCode: number): Promise<AppUpdateInfo> {
|
||||||
const config = getConfig()
|
const config = getConfig()
|
||||||
return apiRequest<AppUpdateInfo>('/api/v1/updates/app/check', {
|
const result = await apiRequest<AppUpdateInfo>('/api/v1/updates/app/check', {
|
||||||
params: {
|
params: {
|
||||||
appId: config.appId,
|
appId: config.appId,
|
||||||
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
|
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
|
||||||
currentVersionCode: String(currentVersionCode),
|
currentVersionCode: String(currentVersionCode),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
downloadUrl: normalizeDownloadUrl(result.downloadUrl),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async openAppStore(appStoreUrl?: string, marketUrl?: string): Promise<void> {
|
async openAppStore(appStoreUrl?: string, marketUrl?: string): Promise<void> {
|
||||||
@ -41,7 +80,7 @@ export const UpdateSDK = {
|
|||||||
|
|
||||||
async checkRnUpdate(moduleId: string, currentVersion: string): Promise<RnUpdateInfo> {
|
async checkRnUpdate(moduleId: string, currentVersion: string): Promise<RnUpdateInfo> {
|
||||||
const config = getConfig()
|
const config = getConfig()
|
||||||
return apiRequest<RnUpdateInfo>('/api/v1/rn/update/check', {
|
const result = await apiRequest<RnUpdateInfo>('/api/v1/rn/update/check', {
|
||||||
params: {
|
params: {
|
||||||
appId: config.appId,
|
appId: config.appId,
|
||||||
moduleId,
|
moduleId,
|
||||||
@ -49,5 +88,34 @@ export const UpdateSDK = {
|
|||||||
currentVersion,
|
currentVersion,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async downloadRnBundle(downloadUrl: string): Promise<string> {
|
||||||
|
const response = await fetch(downloadUrl)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to download bundle: ${response.status}`)
|
||||||
|
}
|
||||||
|
return response.text()
|
||||||
|
},
|
||||||
|
|
||||||
|
async cacheRnBundle(moduleId: string, version: string, md5: string, source: string): Promise<CachedRnBundle> {
|
||||||
|
const payload: CachedRnBundle = {
|
||||||
|
moduleId,
|
||||||
|
version,
|
||||||
|
md5,
|
||||||
|
source,
|
||||||
|
downloadedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
await AsyncStorage.setItem(getBundleCacheKey(moduleId), JSON.stringify(payload))
|
||||||
|
return payload
|
||||||
|
},
|
||||||
|
|
||||||
|
async getCachedRnBundle(moduleId: string): Promise<CachedRnBundle | null> {
|
||||||
|
const raw = await AsyncStorage.getItem(getBundleCacheKey(moduleId))
|
||||||
|
return raw ? JSON.parse(raw) as CachedRnBundle : null
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户