feat(sdk): 按跨平台规范重构所有 SDK 包接口

- common: setUserInfo 成为认证枢纽,分发订阅者;移除 init()/initializeFromLicense();
  initWithConfigFile 解密后调用 initialize() 拉取远程服务配置;XuqmConfig 增加服务开通标志
- push: 移除独立 initialize/registerToken/unregisterToken 等方法;
  改由 _registerUserInfoHandler 订阅,setUserInfo 时自动完成厂商检测+设备注册+token上报
- im: 新增 refreshToken();注册 setUserInfo 订阅,自动登录/断连;移除对 PushSDK 的直接调用
- update: 支持 registerPlugins(批量) + registerPlugin(向后兼容);
  版本号自动从 AsyncStorage 读取,移除 version 字段;新增 updatePlugin(一步完成)、
  downloadAndInstallApk、setBundleCallbacks 注入宿主写入/重载能力
- license: 移除独立 initialize/initializeFromFile;依赖 getConfig() 获取 appKey/licenseUrl
- 顶层 src/sdk.ts: 移除旧 login/logout 包装层,直接重导出 CommonXuqmSDK

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-15 10:57:55 +08:00
父节点 bed221f536
当前提交 b1e8e307d2
共有 12 个文件被更改,包括 445 次插入493 次删除

查看文件

@ -8,32 +8,59 @@ export interface XuqmConfig {
appKey: string appKey: string
apiUrl: string apiUrl: string
imWsUrl: string imWsUrl: string
fileServiceUrl: string // fetched from remote config fileServiceUrl: string
licenseUrl: string
debug: boolean debug: boolean
// 服务开通状态(由平台远程配置决定)
imEnabled: boolean
pushEnabled: boolean
licenseEnabled: boolean
} }
let _config: XuqmConfig | null = null
let _userId: string | null = null
export interface XuqmUserInfo { export interface XuqmUserInfo {
userId?: string userId: string
userSig?: string // IM 登录凭证;未开通 IM 时可不传
name?: string name?: string
email?: string email?: string
phone?: string phone?: string
avatar?: string
} }
// ─── Internal state ────────────────────────────────────────────────────────────
let _config: XuqmConfig | null = null
let _userId: string | null = null
let _userInfo: XuqmUserInfo | null = null let _userInfo: XuqmUserInfo | null = null
const _userInfoHandlers: Array<(info: XuqmUserInfo | null) => void> = []
// ─── Config ────────────────────────────────────────────────────────────────────
export interface XuqmRemoteConfig {
imApiUrl?: string
apiUrl?: string
imWsUrl?: string
fileServiceUrl?: string
licenseUrl?: string
imEnabled?: boolean
pushEnabled?: boolean
licenseEnabled?: boolean
}
export function initConfigFromRemote( export function initConfigFromRemote(
options: XuqmInitOptions, options: XuqmInitOptions,
remote: { imWsUrl: string; fileServiceUrl: string; apiUrl: string }, remote: XuqmRemoteConfig,
): void { ): void {
const apiUrl = remote.imApiUrl ?? remote.apiUrl ?? ''
_config = { _config = {
appKey: options.appKey, appKey: options.appKey,
apiUrl: remote.apiUrl, apiUrl,
imWsUrl: remote.imWsUrl, imWsUrl: remote.imWsUrl ?? '',
fileServiceUrl: remote.fileServiceUrl, fileServiceUrl: remote.fileServiceUrl ?? apiUrl,
licenseUrl: remote.licenseUrl ?? apiUrl,
debug: options.debug ?? false, debug: options.debug ?? false,
imEnabled: remote.imEnabled ?? !!remote.imWsUrl,
pushEnabled: remote.pushEnabled ?? true,
licenseEnabled: remote.licenseEnabled ?? false,
} }
} }
@ -42,6 +69,12 @@ export function getConfig(): XuqmConfig {
return _config return _config
} }
export function isInitialized(): boolean {
return _config !== null
}
// ─── UserId ────────────────────────────────────────────────────────────────────
export function setUserId(userId: string | null): void { export function setUserId(userId: string | null): void {
_userId = userId _userId = userId
} }
@ -50,15 +83,17 @@ export function getUserId(): string | null {
return _userId return _userId
} }
export function isInitialized(): boolean { // ─── UserInfo + subscribers ────────────────────────────────────────────────────
return _config !== null
export function _registerUserInfoHandler(handler: (info: XuqmUserInfo | null) => void): void {
_userInfoHandlers.push(handler)
} }
export function setUserInfo(info: XuqmUserInfo | null): void { export function setUserInfo(info: XuqmUserInfo | null): void {
_userInfo = info _userInfo = info
// Sync userId for backward compatibility _userId = info?.userId ?? null
if (info?.userId) { for (const handler of _userInfoHandlers) {
_userId = info.userId try { handler(info) } catch { /* sub-SDK errors must not break the chain */ }
} }
} }

查看文件

@ -3,7 +3,15 @@ import './autoInit'
export { XuqmSDK } from './sdk' export { XuqmSDK } from './sdk'
export type { XuqmInitOptions, XuqmConfig, XuqmUserInfo } from './config' export type { XuqmInitOptions, XuqmConfig, XuqmUserInfo } from './config'
export { getConfig, isInitialized, setUserId, getUserId } from './config' export {
getConfig,
isInitialized,
setUserId,
getUserId,
setUserInfo,
getUserInfo,
_registerUserInfoHandler,
} from './config'
export { awaitInitialization } from './sdk' export { awaitInitialization } from './sdk'
export { apiRequest, configureHttp, _getToken, _saveToken, _clearToken } from './http' export { apiRequest, configureHttp, _getToken, _saveToken, _clearToken } from './http'
export { DEFAULT_TENANT_PLATFORM_URL, DEFAULT_IM_WS_URL } from './constants' export { DEFAULT_TENANT_PLATFORM_URL, DEFAULT_IM_WS_URL } from './constants'

查看文件

@ -1,5 +1,5 @@
import { initConfigFromRemote, isInitialized, type XuqmInitOptions, type XuqmUserInfo, setUserId as setCommonUserId, getUserId as getCommonUserId, setUserInfo as setCommonUserInfo, getUserInfo as getCommonUserInfo } from './config' import { initConfigFromRemote, isInitialized, type XuqmInitOptions, type XuqmUserInfo, setUserInfo as setCommonUserInfo, getUserInfo as getCommonUserInfo, getUserId as getCommonUserId } from './config'
import { DEFAULT_IM_WS_URL, DEFAULT_TENANT_PLATFORM_URL } from './constants' import { DEFAULT_TENANT_PLATFORM_URL } from './constants'
import { configureHttp } from './http' import { configureHttp } from './http'
import { decryptConfigFile } from './configCrypto' import { decryptConfigFile } from './configCrypto'
@ -36,8 +36,14 @@ function markInitializationFailed(e: unknown): void {
export const XuqmSDK = { export const XuqmSDK = {
/** /**
* @param options.appKey - Your application key (from the tenant platform) * B
* @param options.debug - Enable verbose logging *
* appKey IMPush URL
*
*
* @param options.appKey
* @param options.platformUrl 使
* @param options.debug
*/ */
async initialize(options: XuqmInitOptions): Promise<void> { async initialize(options: XuqmInitOptions): Promise<void> {
if (isInitialized()) return if (isInitialized()) return
@ -46,15 +52,20 @@ export const XuqmSDK = {
const configUrl = `${baseUrl}/api/sdk/config?appKey=${options.appKey}` const configUrl = `${baseUrl}/api/sdk/config?appKey=${options.appKey}`
try { try {
const res = await fetch(configUrl) const res = await fetch(configUrl)
const json = await res.json() if (!res.ok) throw new Error(`[XuqmSDK] Platform config request failed: ${res.status}`)
const remote = json.data ?? json const json = await res.json() as Record<string, unknown>
const remote = (json.data ?? json) as Record<string, unknown>
initConfigFromRemote(options, { initConfigFromRemote(options, {
imWsUrl: remote.imWsUrl, apiUrl: remote.apiUrl as string | undefined,
fileServiceUrl: remote.fileServiceUrl, imWsUrl: remote.imWsUrl as string | undefined,
apiUrl: remote.imApiUrl ?? DEFAULT_TENANT_PLATFORM_URL, fileServiceUrl: remote.fileServiceUrl as string | undefined,
licenseUrl: remote.licenseUrl as string | undefined,
imEnabled: remote.imEnabled as boolean | undefined,
pushEnabled: remote.pushEnabled as boolean | undefined,
licenseEnabled: remote.licenseEnabled as boolean | undefined,
}) })
configureHttp({ configureHttp({
baseUrl: remote.imApiUrl ?? DEFAULT_TENANT_PLATFORM_URL, baseUrl: (remote.apiUrl as string | undefined) ?? baseUrl,
debug: options.debug, debug: options.debug,
}) })
markInitialized() markInitialized()
@ -65,77 +76,36 @@ export const XuqmSDK = {
}, },
/** /**
* @param options.appKey - Your application key (from the tenant platform) * A SDK
* @param options.debug - Enable verbose logging
*/
init(options: XuqmInitOptions): void {
if (isInitialized()) return
initConfigFromRemote(options, {
imWsUrl: DEFAULT_IM_WS_URL,
fileServiceUrl: DEFAULT_TENANT_PLATFORM_URL,
apiUrl: DEFAULT_TENANT_PLATFORM_URL,
})
configureHttp({
baseUrl: DEFAULT_TENANT_PLATFORM_URL,
debug: options.debug,
})
markInitialized()
},
/**
* Initialize from a decrypted license file object.
*/
initializeFromLicense(file: { appKey: string; baseUrl?: string; serverUrl?: string }, options?: { debug?: boolean }): void {
if (isInitialized()) return
const serverUrl = file.serverUrl || file.baseUrl || DEFAULT_TENANT_PLATFORM_URL
initConfigFromRemote({ appKey: file.appKey, debug: options?.debug }, {
imWsUrl: DEFAULT_IM_WS_URL,
fileServiceUrl: serverUrl,
apiUrl: serverUrl,
})
configureHttp({ baseUrl: serverUrl, debug: options?.debug })
markInitialized()
},
/**
* SDK
* *
* 宿 .xuqmconfig SDK * 宿 .xuqmconfig autoInit SDK initialize()
* XUQM-CONFIG-V1 XUQM-LICENSE-V1 * index.ts export
* *
* @param encryptedContent * @param encryptedContent XUQM-CONFIG-V1 XUQM-LICENSE-V1
* @param options.debug
*
* @example
* // common bundle 入口
* import config from './assets/app.xuqmconfig'
* await XuqmSDK.initWithConfigFile(config)
*/ */
async initWithConfigFile(encryptedContent: string, options?: { debug?: boolean }): Promise<void> { async initWithConfigFile(encryptedContent: string, options?: { debug?: boolean }): Promise<void> {
if (isInitialized()) return if (isInitialized()) return
const file = await decryptConfigFile(encryptedContent) const file = await decryptConfigFile(encryptedContent)
this.initializeFromLicense(file, options) // 配置文件解密后包含 appKey 和 platformUrl字段名 serverUrl 或 baseUrl
const platformUrl = file.serverUrl ?? file.baseUrl ?? undefined
await this.initialize({ appKey: file.appKey, platformUrl, debug: options?.debug })
}, },
/** /**
* Wait for initialization to complete. * SDK XuqmSDK
*/ */
async awaitInitialization(): Promise<void> { async awaitInitialization(): Promise<void> {
if (isInitialized()) return if (isInitialized()) return
await ensureInitPromise() await ensureInitPromise()
}, },
setUserId(userId: string | null): void {
setCommonUserId(userId)
},
getUserId(): string | null { getUserId(): string | null {
return getCommonUserId() return getCommonUserId()
}, },
/** /**
* Set user info for gray release targeting and license verification. * SDKPushIMLicense
* Call this after user login. * null SDK
*/ */
setUserInfo(info: XuqmUserInfo | null): void { setUserInfo(info: XuqmUserInfo | null): void {
setCommonUserInfo(info) setCommonUserInfo(info)

查看文件

@ -1,4 +1,5 @@
import { apiRequest, _getToken, _saveToken, getConfig, getDeviceInfo, setUserId as setCommonUserId, getUserId as getCommonUserId } from '@xuqm/rn-common' import { apiRequest, _getToken, _saveToken, getConfig, getDeviceInfo, setUserId as setCommonUserId, getUserId as getCommonUserId, _registerUserInfoHandler } from '@xuqm/rn-common'
import type { XuqmUserInfo } from '@xuqm/rn-common'
import { ImClient } from './ImClient' import { ImClient } from './ImClient'
import { ImDatabase } from './db/ImDatabase' import { ImDatabase } from './db/ImDatabase'
import type { MessageSearchParams } from './db/ImDatabase' import type { MessageSearchParams } from './db/ImDatabase'
@ -317,17 +318,6 @@ export const ImSDK = {
client.addListener({ client.addListener({
onConnected: () => { onConnected: () => {
_syncHistoryForAllConversations().catch(() => {}) _syncHistoryForAllConversations().catch(() => {})
// Auto-register push token if Push module is installed and a token is pending
import('@xuqm/rn-push')
.then(({ PushSDK }) => {
const pending = PushSDK.getPendingToken?.()
if (pending) {
PushSDK.registerToken(userId, pending.token, pending.vendor).catch(() => {})
}
})
.catch(() => {
// Push module not installed — ignore gracefully
})
}, },
}) })
void client.connect() void client.connect()
@ -1504,4 +1494,32 @@ export const ImSDK = {
_currentUserSig = null _currentUserSig = null
setCommonUserId(null) setCommonUserId(null)
}, },
/**
* IM userSig
*/
async refreshToken(userSig: string): Promise<void> {
if (!_currentUserId) throw new Error('[ImSDK] Not logged in — call setUserInfo first.')
await ImSDK.login(_currentUserId, userSig)
},
} }
// ─── 订阅 XuqmSDK.setUserInfo ──────────────────────────────────────────────────
_registerUserInfoHandler(async (info: XuqmUserInfo | null) => {
if (!info) {
ImSDK.disconnect()
return
}
// 仅当 IM 服务已开通且 userSig 存在时自动登录
if (!info.userSig) return
try {
const { getConfig, isInitialized } = await import('@xuqm/rn-common')
if (!isInitialized()) return
const config = getConfig()
if (!config.imEnabled) return
} catch {
return
}
await ImSDK.login(info.userId, info.userSig).catch(() => {})
})

查看文件

@ -1,3 +1,2 @@
export { initialize, initializeFromFile, checkLicense, getStatus, getDeviceId, clear } from './license' export { checkLicense, getStatus, getDeviceId, clear } from './license'
export { decryptLicenseFile, decryptConfigFile } from './crypto' export type { LicenseUserInfo, LicenseStatus, LicenseResult } from './models'
export type { LicenseFile, LicenseUserInfo, LicenseStatus, LicenseResult } from './models'

查看文件

@ -1,56 +1,27 @@
import { Platform } from 'react-native' import { Platform } from 'react-native'
import { getDeviceId as getCommonDeviceId, getDeviceInfo, awaitInitialization } from '@xuqm/rn-common' import { getDeviceId as getCommonDeviceId, getDeviceInfo, awaitInitialization, getConfig, getUserId } from '@xuqm/rn-common'
import { decryptLicenseFile } from './crypto'
import * as store from './store' import * as store from './store'
import type { LicenseFile, LicenseResult, LicenseStatus, LicenseUserInfo, RegisterRequest, RegisterResponse, VerifyRequest, VerifyResponse } from './models' import type { LicenseResult, LicenseStatus, LicenseUserInfo, RegisterRequest, RegisterResponse, VerifyRequest, VerifyResponse } from './models'
const DEFAULT_BASE_URL = 'https://auth.dev.xuqinmin.com'
const CACHE_WINDOW_MS = 10 * 60 * 1000 const CACHE_WINDOW_MS = 10 * 60 * 1000
const STATUS_OK = 'ok' const STATUS_OK = 'ok'
const STATUS_DENIED = 'denied' const STATUS_DENIED = 'denied'
interface LicenseConfig {
appKey: string
baseUrl: string
deviceName?: string
}
let _config: LicenseConfig | null = null
// In-memory cache to avoid repeated AsyncStorage reads within a session
let _cachedStatus: string | null = null let _cachedStatus: string | null = null
let _cachedStatusTime = 0 let _cachedStatusTime = 0
function normalize(url: string): string {
const s = url.trim()
return s.endsWith('/') ? s : s + '/'
}
export function initialize(appKey: string, options: { baseUrl?: string; deviceName?: string } = {}): void {
_config = {
appKey,
baseUrl: normalize(options.baseUrl ?? DEFAULT_BASE_URL),
deviceName: options.deviceName,
}
}
// Auto-initialize from license file asset. The asset is embedded as a string (e.g., via react-native-raw-text or require()).
export async function initializeFromFile(encryptedContent: string): Promise<void> {
const file = await decryptLicenseFile(encryptedContent)
initialize(file.appKey, { baseUrl: file.baseUrl })
}
export async function checkLicense(userInfo?: LicenseUserInfo): Promise<LicenseResult> { export async function checkLicense(userInfo?: LicenseUserInfo): Promise<LicenseResult> {
if (!_config) { // 等待 XuqmSDK 初始化完成,使用公共配置
await awaitInitialization() await awaitInitialization()
if (!_config) return { type: 'error', message: 'LicenseSDK not initialized' } const config = getConfig()
} const { licenseUrl, appKey } = config
const { appKey, baseUrl } = _config const baseUrl = licenseUrl.endsWith('/') ? licenseUrl : licenseUrl + '/'
// In-memory cache check // 内存缓存
if (_cachedStatus === STATUS_OK && Date.now() - _cachedStatusTime < CACHE_WINDOW_MS) { if (_cachedStatus === STATUS_OK && Date.now() - _cachedStatusTime < CACHE_WINDOW_MS) {
return { type: 'success', reason: 'Cached' } return { type: 'success', reason: 'Cached' }
} }
// Persistent cache check // 持久化缓存
const persistedStatus = await store.getStatus() const persistedStatus = await store.getStatus()
const persistedTime = await store.getStatusTime() const persistedTime = await store.getStatusTime()
if (persistedStatus === STATUS_OK && Date.now() - persistedTime < CACHE_WINDOW_MS) { if (persistedStatus === STATUS_OK && Date.now() - persistedTime < CACHE_WINDOW_MS) {
@ -59,37 +30,37 @@ export async function checkLicense(userInfo?: LicenseUserInfo): Promise<LicenseR
return { type: 'success', reason: 'Cached' } return { type: 'success', reason: 'Cached' }
} }
const deviceId = await getOrCreateDeviceId() const deviceId = await _getOrCreateDeviceId()
const deviceInfo = await getDeviceInfo() const deviceInfo = await getDeviceInfo()
try { try {
const storedToken = await store.getToken() const storedToken = await store.getToken()
if (storedToken) { if (storedToken) {
const verifyReq: VerifyRequest = { appKey, deviceId, token: storedToken, userInfo } const verifyReq: VerifyRequest = { appKey, deviceId, token: storedToken, userInfo }
const verifyResp = await post<VerifyResponse>(`${baseUrl}api/license/verify`, verifyReq) const verifyResp = await _post<VerifyResponse>(`${baseUrl}api/license/verify`, verifyReq)
if (verifyResp.valid) { if (verifyResp.valid) {
await persistStatus(STATUS_OK) await _persistStatus(STATUS_OK)
return { type: 'success', reason: 'Verified' } return { type: 'success', reason: 'Verified' }
} }
await store.setToken(null) await store.setToken(null)
} }
const userId = getUserId()
const regReq: RegisterRequest = { const regReq: RegisterRequest = {
appKey, appKey,
deviceId, deviceId,
deviceName: _config.deviceName,
deviceModel: deviceInfo.model, deviceModel: deviceInfo.model,
deviceVendor: Platform.OS === 'ios' ? 'Apple' : deviceInfo.brand, deviceVendor: Platform.OS === 'ios' ? 'Apple' : deviceInfo.brand,
osVersion: `${Platform.OS === 'ios' ? 'iOS' : 'Android'} ${deviceInfo.osVersion}`, osVersion: `${Platform.OS === 'ios' ? 'iOS' : 'Android'} ${deviceInfo.osVersion}`,
userInfo, userInfo: userInfo ?? (userId ? { userId } : undefined),
} }
const regResp = await post<RegisterResponse>(`${baseUrl}api/license/register`, regReq) const regResp = await _post<RegisterResponse>(`${baseUrl}api/license/register`, regReq)
if (regResp.success && regResp.token) { if (regResp.success && regResp.token) {
await store.setToken(regResp.token) await store.setToken(regResp.token)
await persistStatus(STATUS_OK) await _persistStatus(STATUS_OK)
return { type: 'success', reason: 'Registered' } return { type: 'success', reason: 'Registered' }
} }
await persistStatus(STATUS_DENIED) await _persistStatus(STATUS_DENIED)
return { type: 'error', message: regResp.message ?? 'Registration denied' } return { type: 'error', message: regResp.message ?? 'Registration denied' }
} catch (e) { } catch (e) {
if (persistedStatus === STATUS_OK) return { type: 'success', reason: 'Offline - cached ok' } if (persistedStatus === STATUS_OK) return { type: 'success', reason: 'Offline - cached ok' }
@ -105,7 +76,7 @@ export async function getStatus(): Promise<LicenseStatus> {
} }
export async function getDeviceId(): Promise<string> { export async function getDeviceId(): Promise<string> {
return getOrCreateDeviceId() return _getOrCreateDeviceId()
} }
export async function clear(): Promise<void> { export async function clear(): Promise<void> {
@ -114,7 +85,7 @@ export async function clear(): Promise<void> {
await store.clearAll() await store.clearAll()
} }
async function getOrCreateDeviceId(): Promise<string> { async function _getOrCreateDeviceId(): Promise<string> {
const existing = await store.getDeviceId() const existing = await store.getDeviceId()
if (existing) return existing if (existing) return existing
const id = await getCommonDeviceId() const id = await getCommonDeviceId()
@ -122,14 +93,14 @@ async function getOrCreateDeviceId(): Promise<string> {
return id return id
} }
async function persistStatus(status: string): Promise<void> { async function _persistStatus(status: string): Promise<void> {
const now = Date.now() const now = Date.now()
_cachedStatus = status _cachedStatus = status
_cachedStatusTime = now _cachedStatusTime = now
await Promise.all([store.setStatus(status), store.setStatusTime(now)]) await Promise.all([store.setStatus(status), store.setStatusTime(now)])
} }
async function post<T>(url: string, body: unknown): Promise<T> { async function _post<T>(url: string, body: unknown): Promise<T> {
const res = await fetch(url, { const res = await fetch(url, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },

查看文件

@ -1,113 +1,23 @@
import { apiRequest, getConfig, getDeviceInfo, getUserId as getCommonUserId } from '@xuqm/rn-common' import {
import type { PushVendor } from '@xuqm/rn-common' apiRequest,
getConfig,
getDeviceInfo,
getUserId as getCommonUserId,
_registerUserInfoHandler,
} from '@xuqm/rn-common'
import type { PushVendor, XuqmUserInfo } from '@xuqm/rn-common'
import { detectVendorNative, registerPushNative, addPushTokenListener } from './NativePush' import { detectVendorNative, registerPushNative, addPushTokenListener } from './NativePush'
export type { PushVendor } export type { PushVendor }
type PendingDeviceToken = { // ─── Internal state ────────────────────────────────────────────────────────────
token: string
vendor?: PushVendor
}
let currentUserId: string | null = null let _registeredUserId: string | null = null
let currentInitializedUserId: string | null = null
let pendingToken: PendingDeviceToken | null = null
let _tokenUnsubscribe: (() => void) | null = null let _tokenUnsubscribe: (() => void) | null = null
async function registerPendingToken(): Promise<void> { // ─── Internal helpers ──────────────────────────────────────────────────────────
const userId = currentUserId ?? getCommonUserId()
if (!userId || !pendingToken) return
const device = await getDeviceInfo()
const config = getConfig()
await apiRequest('/api/push/register', {
method: 'POST',
params: {
appKey: config.appKey,
userId,
vendor: pendingToken.vendor ?? device.pushVendor,
token: pendingToken.token,
platform: device.platform,
deviceId: device.deviceId,
brand: device.brand,
model: device.model,
osVersion: device.osVersion,
},
})
}
export const PushSDK = { async function _registerDevice(userId: string, token: string, vendor: PushVendor): Promise<void> {
async initialize(userId?: string): Promise<void> {
const nextUserId = userId ?? getCommonUserId()
if (currentInitializedUserId === nextUserId) {
currentUserId = nextUserId
return
}
currentInitializedUserId = nextUserId
currentUserId = nextUserId
await registerPendingToken()
},
/**
* Cache a device token without immediately registering it.
* Call this when the native layer receives a token before the user is logged in.
*/
setPendingToken(token: string, vendor?: PushVendor): void {
pendingToken = { token, vendor }
},
/**
* Get the currently cached pending token, if any.
*/
getPendingToken(): PendingDeviceToken | null {
return pendingToken
},
async setDeviceToken(token: string, vendor?: PushVendor): Promise<void> {
pendingToken = { token, vendor }
await registerPendingToken()
},
/**
* Auto-detect vendor and request native push registration.
* On Android this attempts to register with the vendor SDK (Huawei, Xiaomi, OPPO, vivo, Honor, FCM).
* On iOS this requests APNs registration.
* Listen for token updates via onPushToken(callback).
*/
async requestNativeRegistration(): Promise<void> {
await registerPushNative()
},
/**
* Listen for push tokens from the native layer.
* Call setDeviceToken(token, vendor) inside the callback to register with the server.
*/
onPushToken(callback: (token: string, vendor: string) => void): () => void {
if (_tokenUnsubscribe) {
_tokenUnsubscribe()
_tokenUnsubscribe = null
}
_tokenUnsubscribe = addPushTokenListener((event) => {
callback(event.token, event.vendor)
})
return () => {
if (_tokenUnsubscribe) {
_tokenUnsubscribe()
_tokenUnsubscribe = null
}
}
},
/**
* Register a push device token for the given user.
* If vendor is omitted, it is auto-detected from the device brand.
*
* @param userId - The logged-in user's ID
* @param token - The device push token from the vendor SDK
* @param vendor - Optional; auto-detected when not provided
*/
async registerToken(userId: string, token: string, vendor?: PushVendor): Promise<void> {
currentUserId = userId
pendingToken = { token, vendor }
const config = getConfig() const config = getConfig()
const device = await getDeviceInfo() const device = await getDeviceInfo()
await apiRequest('/api/push/register', { await apiRequest('/api/push/register', {
@ -115,7 +25,7 @@ export const PushSDK = {
params: { params: {
appKey: config.appKey, appKey: config.appKey,
userId, userId,
vendor: vendor ?? device.pushVendor, vendor,
token, token,
platform: device.platform, platform: device.platform,
deviceId: device.deviceId, deviceId: device.deviceId,
@ -124,24 +34,106 @@ export const PushSDK = {
osVersion: device.osVersion, osVersion: device.osVersion,
}, },
}) })
pendingToken = null _registeredUserId = userId
}, }
async unregisterToken(userId: string): Promise<void> { async function _unregisterDevice(userId: string): Promise<void> {
const config = getConfig() const config = getConfig()
const { deviceId } = await getDeviceInfo() const { deviceId } = await getDeviceInfo()
await apiRequest('/api/push/unregister', { await apiRequest('/api/push/unregister', {
method: 'DELETE', method: 'DELETE',
params: { appKey: config.appKey, userId, deviceId }, params: { appKey: config.appKey, userId, deviceId },
}) })
if (currentUserId === userId) currentUserId = null if (_registeredUserId === userId) _registeredUserId = null
if (currentInitializedUserId === userId) currentInitializedUserId = null }
if (pendingToken && currentUserId === null) pendingToken = null
async function _startRegistrationFlow(userId: string): Promise<void> {
// 1. 检测厂商并请求原生注册
await registerPushNative()
// 2. 监听 token 回调,收到后自动上报
if (_tokenUnsubscribe) _tokenUnsubscribe()
_tokenUnsubscribe = addPushTokenListener(async (event) => {
const currentUserId = getCommonUserId()
if (!currentUserId) return
const vendor = (await detectVendorNative()) as PushVendor ?? (event.vendor as PushVendor)
await _registerDevice(currentUserId, event.token, vendor).catch(() => {})
})
}
// ─── 订阅 XuqmSDK.setUserInfo ──────────────────────────────────────────────────
_registerUserInfoHandler(async (info: XuqmUserInfo | null) => {
if (!info) {
// 登出:解绑 token
const userId = _registeredUserId
if (userId) {
await _unregisterDevice(userId).catch(() => {})
}
if (_tokenUnsubscribe) {
_tokenUnsubscribe()
_tokenUnsubscribe = null
}
return
}
// 登录:启动设备注册流程(幂等,同一用户不重复注册)
if (_registeredUserId === info.userId) return
await _startRegistrationFlow(info.userId).catch(() => {})
})
// ─── PushSDK 公开 API ──────────────────────────────────────────────────────────
export const PushSDK = {
/**
* 线
*/
async setOfflinePushEnabled(enabled: boolean): Promise<void> {
const config = getConfig()
const userId = getCommonUserId()
if (!userId) return
await apiRequest('/api/push/settings/offline', {
method: 'PUT',
params: { appKey: config.appKey, userId, enabled: String(enabled) },
})
}, },
async logout(userId?: string): Promise<void> { /**
const targetUserId = userId ?? currentUserId ?? getCommonUserId() * 24 '22:00''08:00'
if (!targetUserId) return */
await PushSDK.unregisterToken(targetUserId) async setQuietHours(start: string, end: string): Promise<void> {
const config = getConfig()
const userId = getCommonUserId()
if (!userId) return
await apiRequest('/api/push/settings/quiet-hours', {
method: 'PUT',
params: { appKey: config.appKey, userId, start, end },
})
},
/**
*
*/
async clearQuietHours(): Promise<void> {
const config = getConfig()
const userId = getCommonUserId()
if (!userId) return
await apiRequest('/api/push/settings/quiet-hours', {
method: 'DELETE',
params: { appKey: config.appKey, userId },
})
},
/**
* XuqmSDK.setUserInfo(null)
*/
async logout(): Promise<void> {
const userId = _registeredUserId ?? getCommonUserId()
if (!userId) return
await _unregisterDevice(userId).catch(() => {})
if (_tokenUnsubscribe) {
_tokenUnsubscribe()
_tokenUnsubscribe = null
}
}, },
} }

查看文件

@ -1,3 +1,3 @@
export { PushSDK } from './PushSDK' export { PushSDK } from './PushSDK'
export type { PushVendor } from './PushSDK' export type { PushVendor } from './PushSDK'
export { isNativePushAvailable, detectVendorNative, registerPushNative, addPushTokenListener } from './NativePush' export { isNativePushAvailable } from './NativePush'

查看文件

@ -1,75 +1,48 @@
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import { Linking, Platform } from 'react-native' import { Linking, Platform } from 'react-native'
import { apiRequest, getConfig, getUserId } from '@xuqm/rn-common' import { apiRequest, getConfig, getUserId, _registerUserInfoHandler } from '@xuqm/rn-common'
import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion' import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion'
import { awaitInitialization } from '@xuqm/rn-common'
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types (public) ────────────────────────────────────────────────────────────
/** 插件注册元数据 */ /** 插件注册(版本号由 SDK 自动获取,不需要 App 传入) */
export interface PluginMeta { export interface PluginRegistration {
/** 插件唯一标识,如 'buz1'、'buz2' */
moduleId: string moduleId: string
/** 当前 bundle 版本号 */
version: string
} }
/** /** @deprecated 使用 PluginRegistration移除 version 字段) */
* App export interface PluginMeta extends PluginRegistration {
* version?: string
* Android SDK UpdateInfo }
* SDK UI app
*/
export interface AppUpdateInfo { export interface AppUpdateInfo {
/** 是否需要更新 */
needsUpdate: boolean needsUpdate: boolean
/** 最新版本名,如 '2.1.0' */
versionName?: string versionName?: string
/** 最新版本号(整数) */
versionCode?: number versionCode?: number
/** APK/安装包直接下载地址Android 有此字段时优先下载) */
downloadUrl?: string downloadUrl?: string
/** 更新日志 */
changeLog?: string changeLog?: string
/** 是否强制更新true 时不允许跳过) */
forceUpdate?: boolean forceUpdate?: boolean
/** iOS App Store 地址 */
appStoreUrl?: string appStoreUrl?: string
/** Android 应用商店地址(华为/小米/OPPO 等) */
marketUrl?: string marketUrl?: string
/** 服务端要求登录后才能检查true 时 needsUpdate 通常为 false */
requiresLogin?: boolean requiresLogin?: boolean
/** SDK 内部标记APK 是否已下载到本地(仅 Android */
alreadyDownloaded?: boolean alreadyDownloaded?: boolean
/** APK 文件 SHA-256 校验值 */
apkHash?: string | null apkHash?: string | null
} }
/**
* RN Bundle
*
* = appKey + platform + moduleId
* - appKey
* - platformANDROID / IOS
* - moduleId ID 'buz1'
*/
export interface PluginUpdateInfo { export interface PluginUpdateInfo {
/** 是否需要更新 */
needsUpdate: boolean needsUpdate: boolean
/** 最新版本号 */
latestVersion: string latestVersion: string
/** bundle 下载地址 */ currentVersion: string
downloadUrl: string downloadUrl: string
/** bundle 文件 MD5 */
md5: string md5: string
/** 要求的最低 common bundle 版本 */
minCommonVersion: string minCommonVersion: string
/** 更新说明 */
note: string note: string
forceUpdate?: boolean
changeLog?: string
} }
/** 已缓存的 bundle 元数据 */ export type { CachedRnBundle }
export interface CachedRnBundle { interface CachedRnBundle {
moduleId: string moduleId: string
version: string version: string
md5: string md5: string
@ -77,14 +50,24 @@ export interface CachedRnBundle {
source: string source: string
} }
// ─── Internal ───────────────────────────────────────────────────────────────── // ─── Internal state ────────────────────────────────────────────────────────────
const _pluginRegistry = new Map<string, PluginMeta>() const _pluginRegistry = new Set<string>()
let _writeBundleCallback: ((moduleId: string, source: string) => Promise<void>) | null = null
let _reloadBundleCallback: ((moduleId: string) => Promise<void>) | null = null
function bundleCacheKey(moduleId: string) { function bundleCacheKey(moduleId: string) {
return `@xuqm:bundle:${moduleId}` return `@xuqm:bundle:${moduleId}`
} }
async function _getCachedVersion(moduleId: string): Promise<string> {
const raw = await AsyncStorage.getItem(bundleCacheKey(moduleId)).catch(() => null)
if (!raw) return '0.0.0'
const cached = JSON.parse(raw) as CachedRnBundle
return cached.version ?? '0.0.0'
}
function normalizeDownloadUrl(rawUrl?: string): string | undefined { function normalizeDownloadUrl(rawUrl?: string): string | undefined {
if (!rawUrl) return rawUrl if (!rawUrl) return rawUrl
if (rawUrl.includes('/api/v1/updates/api/v1/rn/files/')) { if (rawUrl.includes('/api/v1/updates/api/v1/rn/files/')) {
@ -101,66 +84,117 @@ function normalizeDownloadUrl(rawUrl?: string): string | undefined {
return rawUrl return rawUrl
} }
async function _downloadText(url: string, onProgress?: (p: number) => void): Promise<string> {
const response = await fetch(url)
if (!response.ok) throw new Error(`[UpdateSDK] Download failed: ${response.status}`)
if (!onProgress) return response.text()
const contentLength = Number(response.headers.get('Content-Length') ?? '0')
const reader = response.body?.getReader()
if (!reader) return response.text()
const chunks: Uint8Array[] = []
let received = 0
for (;;) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
received += value.length
if (contentLength > 0) onProgress(received / contentLength)
}
const decoder = new TextDecoder()
return chunks.map(c => decoder.decode(c, { stream: true })).join('') + decoder.decode()
}
async function _downloadBinary(url: string, onProgress?: (p: number) => void): Promise<ArrayBuffer> {
const response = await fetch(url)
if (!response.ok) throw new Error(`[UpdateSDK] Download failed: ${response.status}`)
if (!onProgress) return response.arrayBuffer()
const contentLength = Number(response.headers.get('Content-Length') ?? '0')
const reader = response.body?.getReader()
if (!reader) return response.arrayBuffer()
const chunks: Uint8Array[] = []
let received = 0
for (;;) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
received += value.length
if (contentLength > 0) onProgress(received / contentLength)
}
const totalLength = chunks.reduce((acc, c) => acc + c.length, 0)
const combined = new Uint8Array(totalLength)
let offset = 0
for (const chunk of chunks) {
combined.set(chunk, offset)
offset += chunk.length
}
return combined.buffer
}
// ─── 订阅 setUserInfouserId 用于定向更新) ──────────────────────────────────
_registerUserInfoHandler(() => {
// updatePlugin 检查更新时会实时调用 getUserId(),此处无需额外操作
})
// ─── UpdateSDK ──────────────────────────────────────────────────────────────── // ─── UpdateSDK ────────────────────────────────────────────────────────────────
export const UpdateSDK = { export const UpdateSDK = {
// ── 宿主注入(插件化写入/重载能力)──────────────────────────────────────────
/**
* bundle
* 宿 src/core/updater.ts BundleRuntime
*
* @example
* UpdateSDK.setBundleCallbacks({
* writeBundle: writeBundleFile,
* reloadBundle: loadBundle,
* })
*/
setBundleCallbacks(callbacks: {
writeBundle: (moduleId: string, source: string) => Promise<void>
reloadBundle: (moduleId: string) => Promise<void>
}): void {
_writeBundleCallback = callbacks.writeBundle
_reloadBundleCallback = callbacks.reloadBundle
},
// ── 插件注册 ────────────────────────────────────────────────────────────── // ── 插件注册 ──────────────────────────────────────────────────────────────
/** /**
* bundle * SDK App
*
* = moduleId 'buz1'
* = appKey+ platform+ moduleId
* *
* @example * @example
* // src/plugins/buz1/bundle.ts * UpdateSDK.registerPlugins([{ moduleId: 'buz1' }, { moduleId: 'buz2' }])
* UpdateSDK.registerPlugin({ moduleId: 'buz1', version: '1.0.0' })
*/ */
registerPlugin(meta: PluginMeta): void { registerPlugins(plugins: PluginRegistration[]): void {
_pluginRegistry.set(meta.moduleId, meta) for (const p of plugins) {
_pluginRegistry.add(p.moduleId)
}
}, },
/** /**
* * version SDK
*/ */
getRegisteredPluginVersion(moduleId: string): string | undefined { registerPlugin(meta: PluginRegistration): void {
return _pluginRegistry.get(moduleId)?.version _pluginRegistry.add(meta.moduleId)
}, },
/** getRegisteredPlugins(): string[] {
* return Array.from(_pluginRegistry)
*/
getRegisteredPlugins(): PluginMeta[] {
return Array.from(_pluginRegistry.values())
}, },
// ── App 整包更新 ────────────────────────────────────────────────────────── // ── App 整包更新 ──────────────────────────────────────────────────────────
/**
* App
*
* SDK iOS/Android
* /
*
* Android SDK UpdateSDK.checkAppUpdate()
* - app UI
* - Android downloadUrl APK downloadUrl marketUrl
* - iOS appStoreUrl App Store
*
* @param bypassIgnore false=
* true =
*/
async checkAppUpdate(bypassIgnore?: boolean): Promise<AppUpdateInfo> { async checkAppUpdate(bypassIgnore?: boolean): Promise<AppUpdateInfo> {
await awaitInitialization()
const config = getConfig() const config = getConfig()
const currentVersionCode = getAppVersionCode()
const userId = getUserId()?.trim()
const params: Record<string, string> = { const params: Record<string, string> = {
appKey: config.appKey, appKey: config.appKey,
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
currentVersionCode: String(currentVersionCode), currentVersionCode: String(getAppVersionCode()),
} }
const userId = getUserId()?.trim()
if (userId) params.userId = userId if (userId) params.userId = userId
if (bypassIgnore) params.bypassIgnore = 'true' if (bypassIgnore) params.bypassIgnore = 'true'
@ -171,130 +205,117 @@ export const UpdateSDK = {
return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) } return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) }
}, },
/**
*
* iOS 使 appStoreUrlAndroid 使 marketUrl
*/
async openStore(appStoreUrl?: string, marketUrl?: string): Promise<void> { async openStore(appStoreUrl?: string, marketUrl?: string): Promise<void> {
const url = Platform.OS === 'ios' ? appStoreUrl : marketUrl const url = Platform.OS === 'ios' ? appStoreUrl : marketUrl
if (url) await Linking.openURL(url) if (url) await Linking.openURL(url)
}, },
// ── 插件更新 ────────────────────────────────────────────────────────────── /**
* Android APK
*/
async downloadAndInstallApk(
downloadUrl: string,
options?: {
onProgress?: (progress: number) => void
sha256?: string
},
): Promise<void> {
if (Platform.OS !== 'android') throw new Error('[UpdateSDK] APK install is Android-only.')
// NativeApkInstall 由宿主提供TurboModule,若未注入则 fallback 到 Linking
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { XuqmApkInstall } = require('./NativeApkInstall') as { XuqmApkInstall: { installApk(url: string, sha256?: string | null): Promise<void> } }
await XuqmApkInstall.installApk(downloadUrl, options?.sha256 ?? null)
} catch {
// Native module not available — fallback to Linking
await Linking.openURL(downloadUrl)
}
},
// ── 插件Bundle热更新 ──────────────────────────────────────────────────
/** /**
* *
* *
* = appKey + platform + moduleId * SDK AsyncStorage 0.0.0
* registerPlugin() * registerPlugin / registerPlugins
*
* @param moduleId ID 'buz1'
* @returns latestVersion / downloadUrl / md5
* @throws
*/ */
async checkPluginUpdate(moduleId: string): Promise<PluginUpdateInfo> { async checkPluginUpdate(moduleId: string): Promise<PluginUpdateInfo> {
await awaitInitialization() if (!_pluginRegistry.has(moduleId)) {
const config = getConfig()
const meta = _pluginRegistry.get(moduleId)
if (!meta) {
throw new Error( throw new Error(
`[UpdateSDK] Plugin "${moduleId}" not registered. ` + `[UpdateSDK] Plugin "${moduleId}" not registered. ` +
'Call UpdateSDK.registerPlugin({ moduleId, version }) at bundle load time.', 'Call UpdateSDK.registerPlugins([{ moduleId }]) first.',
) )
} }
const result = await apiRequest<PluginUpdateInfo>('/api/v1/rn/update/check', { const config = getConfig()
skipAuth: true, const currentVersion = await _getCachedVersion(moduleId)
params: { const userId = getUserId()?.trim()
const params: Record<string, string> = {
appKey: config.appKey, appKey: config.appKey,
moduleId, moduleId,
platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS',
currentVersion: meta.version, currentVersion,
}, }
if (userId) params.userId = userId
const result = await apiRequest<PluginUpdateInfo>('/api/v1/rn/update/check', {
skipAuth: true,
params,
}) })
return { ...result, downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl } return {
...result,
currentVersion,
downloadUrl: normalizeDownloadUrl(result.downloadUrl) ?? result.downloadUrl,
}
}, },
/** /**
* bundle *
* *
* @param downloadUrl * SDK bundle silent = false
* @param onProgress (0~1) *
* setBundleCallbacks /
*
* @param silent true = false=
*/ */
async downloadPluginBundle(downloadUrl: string, onProgress?: (progress: number) => void): Promise<string> { async updatePlugin(
const response = await fetch(downloadUrl) moduleId: string,
if (!response.ok) throw new Error(`[UpdateSDK] Bundle download failed: ${response.status}`) options?: {
onProgress?: (progress: number) => void
if (!onProgress) return response.text() silent?: boolean
const contentLength = Number(response.headers.get('Content-Length') ?? '0')
const reader = response.body?.getReader()
if (!reader) return response.text()
const chunks: Uint8Array[] = []
let received = 0
for (;;) {
const { done, value } = await reader.read()
if (done) break
chunks.push(value)
received += value.length
if (contentLength > 0) {
onProgress(received / contentLength)
}
}
const decoder = new TextDecoder()
return chunks.map(chunk => decoder.decode(chunk, { stream: true })).join('') + decoder.decode()
}, },
): Promise<void> {
const info = await UpdateSDK.checkPluginUpdate(moduleId)
if (!info.needsUpdate) return
/** const source = await _downloadText(info.downloadUrl, options?.onProgress)
* bundle AsyncStorage
*/ // 写入 AsyncStorage 缓存(版本记录)
async cachePluginBundle(moduleId: string, version: string, md5: string, source: string): Promise<CachedRnBundle> { const cachePayload: CachedRnBundle = {
const payload: CachedRnBundle = { moduleId,
moduleId, version, md5, source, version: info.latestVersion,
md5: info.md5,
source,
downloadedAt: new Date().toISOString(), downloadedAt: new Date().toISOString(),
} }
await AsyncStorage.setItem(bundleCacheKey(moduleId), JSON.stringify(payload)) await AsyncStorage.setItem(bundleCacheKey(moduleId), JSON.stringify(cachePayload))
return payload
},
/** // 写入宿主文件系统(需注入 setBundleCallbacks
* bundle if (_writeBundleCallback) {
*/ await _writeBundleCallback(moduleId, source)
async getCachedPluginBundle(moduleId: string): Promise<CachedRnBundle | null> { }
const raw = await AsyncStorage.getItem(bundleCacheKey(moduleId))
return raw ? (JSON.parse(raw) as CachedRnBundle) : null
},
/** // 触发重载(非静默模式)
* if (!options?.silent && _reloadBundleCallback) {
* await _reloadBundleCallback(moduleId)
* bundle }
* reloadPlugin()
*
* @param moduleId ID
* @param onProgress (0~1)
* @returns bundle null
*/
async checkAndCachePlugin(moduleId: string, onProgress?: (progress: number) => void): Promise<CachedRnBundle | null> {
const info = await this.checkPluginUpdate(moduleId)
if (!info.needsUpdate) return null
const source = await this.downloadPluginBundle(info.downloadUrl, onProgress)
return this.cachePluginBundle(moduleId, info.latestVersion, info.md5, source)
}, },
// ── 版本信息 ────────────────────────────────────────────────────────────── // ── 版本信息 ──────────────────────────────────────────────────────────────
/** 当前 App versionCode原生读取 */
getAppVersionCode, getAppVersionCode,
/** 当前 App versionName原生读取 */
getAppVersionName, getAppVersionName,
/**
*
*/
_devSetAppVersion(versionCode: number, versionName?: string): void { _devSetAppVersion(versionCode: number, versionName?: string): void {
_devSetAppVersion(versionCode, versionName) _devSetAppVersion(versionCode, versionName)
}, },

查看文件

@ -1,3 +1,2 @@
export { UpdateSDK } from './UpdateSDK' export { UpdateSDK } from './UpdateSDK'
export type { PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from './UpdateSDK' export type { PluginRegistration, PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from './UpdateSDK'
export { NativeBundle } from './NativeBundle'

查看文件

@ -1,7 +1,6 @@
export { XuqmSDK } from './sdk' export { XuqmSDK } from './sdk'
export type { UnifiedLoginOptions } from './sdk' export type { XuqmInitOptions, XuqmUserInfo, DeviceInfo, XuqmConfig } from '@xuqm/rn-common'
export type { XuqmInitOptions, XuqmUserInfo, DeviceInfo } from '@xuqm/rn-common' export { getDeviceId, getDeviceInfo, detectPushVendor, setUserId, getUserId, setUserInfo, getUserInfo } from '@xuqm/rn-common'
export { getDeviceId, getDeviceInfo, detectPushVendor, setUserId, getUserId } from '@xuqm/rn-common'
export { ScaledImage } from '@xuqm/rn-common' export { ScaledImage } from '@xuqm/rn-common'
export { apiRequest } from '@xuqm/rn-common' export { apiRequest } from '@xuqm/rn-common'
export { ImSDK, ImClient, ImDatabase, uploadFile } from '@xuqm/rn-im' export { ImSDK, ImClient, ImDatabase, uploadFile } from '@xuqm/rn-im'
@ -24,7 +23,7 @@ export type {
export { PushSDK } from '@xuqm/rn-push' export { PushSDK } from '@xuqm/rn-push'
export type { PushVendor } from '@xuqm/rn-push' export type { PushVendor } from '@xuqm/rn-push'
export { UpdateSDK } from '@xuqm/rn-update' export { UpdateSDK } from '@xuqm/rn-update'
export type { PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from '@xuqm/rn-update' export type { PluginRegistration, PluginMeta, AppUpdateInfo, PluginUpdateInfo, CachedRnBundle } from '@xuqm/rn-update'
export { export {
XWebViewControl, XWebViewControl,
XWebViewScreen, XWebViewScreen,

查看文件

@ -1,63 +1,3 @@
import { _clearToken, setUserId as setCommonUserId, getUserId as getCommonUserId, XuqmSDK as CommonXuqmSDK } from '@xuqm/rn-common' // XuqmSDK 统一入口 — 直接重新导出 common 层的 XuqmSDK。
import { ImSDK } from '@xuqm/rn-im' // 原 login/logout 包装层已移除;用户认证统一通过 XuqmSDK.setUserInfo() 完成。
import { PushSDK } from '@xuqm/rn-push' export { XuqmSDK } from '@xuqm/rn-common'
export interface UnifiedLoginOptions {
userId: string
userSig: string
}
let currentSession: UnifiedLoginOptions | null = null
async function applyLoginSession(session: UnifiedLoginOptions): Promise<void> {
setCommonUserId(session.userId)
try {
await ImSDK.login(session.userId, session.userSig)
} catch (error) {
setCommonUserId(null)
currentSession = null
throw error
}
currentSession = session
try {
await PushSDK.initialize(session.userId)
} catch (error) {
void error
}
}
async function login(options: UnifiedLoginOptions): Promise<void> {
if (
currentSession &&
currentSession.userId === options.userId &&
currentSession.userSig === options.userSig
) {
setCommonUserId(options.userId)
return
}
if (currentSession) {
await logout()
}
await applyLoginSession(options)
}
async function logout(): Promise<void> {
const userId = currentSession?.userId ?? getCommonUserId()
currentSession = null
setCommonUserId(null)
if (userId) {
try {
await PushSDK.logout(userId)
} catch (error) {
void error
}
}
ImSDK.disconnect()
await _clearToken()
}
export const XuqmSDK = {
...CommonXuqmSDK,
login,
logout,
}