/** * OmniCard AI (CRMmp) - 名片主 NFC 贴卡激活业务驱动层 * H5 无天线走手动 UID;APP 经 uni.getNFCAdapter 尝试读芯片 UID,失败回退手动输入 */ import { NFCRecord } from '../../../common/types.uts'; import { bindNfcDevice, activateNfcDevice } from '../../../common/api.uts'; import { getCurrentLocale, getOmniText } from '../../../common/locale.uts'; export type ScanCopyKey = | 'title' | 'desc' | 'uidLabel' | 'uidPlaceholder' | 'cardIdLabel' | 'cardIdPlaceholder' | 'cta' | 'empty' | 'uidRequired' | 'cardIdRequired' | 'nfcUnavailable' | 'success' | 'error' | 'retry' | 'writeSuccess' | 'writeFailed' | 'writeUnavailable' | 'writeCancelled'; export type ScanState = { uid: string; cardIdRaw: string; deviceCardId: number | null; submitting: boolean; scanning: boolean; manualInput: boolean; errorMessage: string; empty: boolean; record: NFCRecord | null; }; export type ScanPageOptions = { cardId?: string; uid?: string; id?: string; }; export type ActivateByTapInput = { uid: string; cardId?: string | number | null; deviceCardId?: string | number | null; }; function isZhLocale(): boolean { return getCurrentLocale() === 'zh-CN'; } /** * 页面文案:能复用 locale.uts 的走词典,缺 key 时中英本地词典 */ export function resolveScanText(key: ScanCopyKey): string { const locale = getCurrentLocale(); if (key === 'title') { return getOmniText(locale, 'card.scan_nfc'); } if (key === 'success') { return getOmniText(locale, 'common.success'); } if (key === 'error') { return getOmniText(locale, 'common.error'); } if (key === 'empty') { return getOmniText(locale, 'common.empty'); } const zh = isZhLocale(); if (key === 'desc') { return zh ? '贴近实体卡,或手动填写芯片 UID 完成绑定' : 'Hold the card to this phone, or enter the chip UID to bind it'; } if (key === 'uidLabel') { return zh ? '芯片 UID' : 'Chip UID'; } if (key === 'uidPlaceholder') { return zh ? '输入芯片 UID' : 'Enter chip UID'; } if (key === 'cardIdLabel') { return zh ? '名片编号' : 'Card ID'; } if (key === 'cardIdPlaceholder') { return zh ? '输入名片编号' : 'Enter card ID'; } if (key === 'cta') { return zh ? '激活并绑定' : 'Activate and bind'; } if (key === 'uidRequired') { return zh ? '请填写芯片 UID' : 'Enter the chip UID'; } if (key === 'cardIdRequired') { return zh ? '请填写名片编号' : 'Enter a card ID'; } if (key === 'nfcUnavailable') { return zh ? '当前无法读取 NFC,请手动填写 UID' : 'NFC is unavailable. Enter the UID instead'; } if (key === 'retry') { return zh ? '重试' : 'Try again'; } if (key === 'writeSuccess') { return zh ? '已写入' : 'Written to tag'; } if (key === 'writeFailed') { return zh ? '写入失败' : 'Could not write'; } if (key === 'writeUnavailable') { return zh ? '暂无可写内容' : 'Nothing to write'; } if (key === 'writeCancelled') { return zh ? '已取消写入' : 'Write cancelled'; } return ''; } /** * 去空。空串视为非法 UID */ export function parseUid(raw: string | number | null | undefined): string { if (raw == null) { return ''; } return String(raw).trim(); } /** * 解析为正整数。非正整数返回 null */ export function parseCardId(raw: string | number | null | undefined): number | null { if (raw == null) { return null; } if (typeof raw === 'number') { if (raw <= 0) { return null; } if (Math.floor(raw) !== raw) { return null; } return raw; } const text = String(raw).trim(); if (text === '') { return null; } if (!/^[1-9]\d*$/.test(text)) { return null; } return Number(text); } function readErrorMessage(err: unknown): string { if (err instanceof Error && err.message !== '') { return err.message; } return resolveScanText('error'); } /** * 贴卡激活:有入参 cardId 则 bind;设备已有 cardId 且入参为空则 activate */ export async function activateByTap( input: ActivateByTapInput, existing?: { cardId?: string | number | null } | null ): Promise { const uid = parseUid(input.uid); if (uid === '') { throw new Error(resolveScanText('uidRequired')); } const incomingCardId = parseCardId(input.cardId); const fromInputDevice = parseCardId(input.deviceCardId); const fromExisting = existing != null ? parseCardId(existing.cardId) : null; const deviceCardId = fromInputDevice != null ? fromInputDevice : fromExisting; try { if (incomingCardId == null && deviceCardId != null) { return await activateNfcDevice({ uid }); } if (incomingCardId != null) { return await bindNfcDevice({ uid, cardId: incomingCardId }); } throw new Error(resolveScanText('cardIdRequired')); } catch (err) { throw new Error(readErrorMessage(err)); } } export function createScanState(): ScanState { return { uid: '', cardIdRaw: '', deviceCardId: null, submitting: false, scanning: false, manualInput: true, errorMessage: '', empty: true, record: null, }; } export function applyScanOptions(state: ScanState, options: ScanPageOptions | null): void { if (options == null) { return; } if (options.uid != null) { state.uid = parseUid(options.uid); } const cardRaw = options.cardId != null && options.cardId !== '' ? options.cardId : options.id != null ? options.id : ''; if (cardRaw !== '') { state.cardIdRaw = String(cardRaw).trim(); } state.empty = parseUid(state.uid) === ''; } /** * NFC 标签 id(ArrayBuffer)转大写 Hex UID;空则返回空串 */ function nfcIdToHex(id: ArrayBuffer | null | undefined): string { if (id == null) { return ''; } const bytes = new Uint8Array(id); let hex = ''; let i = 0; const len = bytes.length; while (i < len) { let part = bytes[i].toString(16); if (part.length < 2) { part = '0' + part; } hex += part; i++; } return hex.toUpperCase(); } type NfcFail = { errMsg?: string; }; type NfcIdResult = { id?: ArrayBuffer | null; }; type NfcCallbackOptions = { success?: () => void; fail?: (err: NfcFail) => void; }; type NdefWriteOptions = { uris?: string[]; success?: () => void; fail?: (err: NfcFail) => void; }; type NdefWriter = { writeNdefMessage: (options: NdefWriteOptions) => void; connect?: (options: NfcCallbackOptions) => void; }; type NfcAdapter = { getNdef: () => NdefWriter | null; startDiscovery: (options: NfcCallbackOptions) => void; onDiscovered: (callback: (res: NfcIdResult) => void) => void; }; type UniWithNfc = { getNFCAdapter?: () => NfcAdapter | null; }; function readNfcAdapter(): NfcAdapter | null { const runtime = uni as UniWithNfc; if (runtime.getNFCAdapter == null) { return null; } return runtime.getNFCAdapter(); } /** * APP:调用官方风格 uni.getNFCAdapter + startDiscovery / onDiscovered 尝试读 UID。 * 同步路径仅在已读到合法 UID 时返回非空;读不到、API 不存在或抛错返回空串,禁止伪造。 */ function tryReadUidFromAntenna(state: ScanState): string { // #ifdef APP try { const adapter = readNfcAdapter(); if (adapter == null) { return ''; } let syncUid = ''; adapter.startDiscovery({ success: () => { adapter.onDiscovered((res: NfcIdResult) => { const uid = parseUid(nfcIdToHex(res.id)); if (uid === '') { return; } // 异步贴卡成功:写入真实 UID,不伪造 state.uid = uid; state.empty = false; state.scanning = false; state.manualInput = false; state.errorMessage = ''; syncUid = uid; }); }, fail: (_err: { errMsg?: string }) => { // 由 startScan 统一走手动输入回退 }, }); return parseUid(syncUid); } catch (_err) { return ''; } // #endif return ''; } /** * APP 尝试官方 NFC Adapter 读 UID;仅合法 UID 才成功。否则回退手动输入,禁止伪造成功 */ export function startScan(state: ScanState): boolean { // #ifdef APP try { const uid = tryReadUidFromAntenna(state); if (parseUid(uid) !== '') { state.uid = parseUid(uid); state.scanning = false; state.manualInput = false; state.empty = false; state.errorMessage = ''; state.record = null; return true; } } catch (_err) { // 落入下方失败回退 } // #endif state.scanning = false; state.manualInput = true; state.record = null; if (parseUid(state.uid) === '') { state.empty = true; state.errorMessage = resolveScanText('nfcUnavailable'); } return false; } export function resolveStatusLine(state: ScanState): string { if (state.errorMessage !== '') { return state.errorMessage; } if (state.empty && parseUid(state.uid) === '') { return resolveScanText('empty'); } return ''; } /** * 页面主 CTA:校验后走 activateByTap,失败写入一句错误 */ export async function submitScan(state: ScanState): Promise { if (state.submitting) { return null; } const uid = parseUid(state.uid); if (uid === '') { state.empty = true; state.errorMessage = resolveScanText('uidRequired'); return null; } state.uid = uid; state.empty = false; state.submitting = true; state.errorMessage = ''; try { const record = await activateByTap( { uid, cardId: state.cardIdRaw, deviceCardId: state.deviceCardId, }, { cardId: state.deviceCardId } ); state.submitting = false; state.record = record; if (record.cardId != null) { const bound = parseCardId(record.cardId); if (bound != null) { state.deviceCardId = bound; } } return record; } catch (err) { state.submitting = false; state.record = null; state.errorMessage = readErrorMessage(err); return null; } } /** * 可写 NDEF URI:有合法 cardId 才生成;否则空串表示无可写内容 */ export function buildNdefUri(cardId: string | number | null | undefined): string { const id = parseCardId(cardId); if (id == null) { return ''; } return `https://omnicard.ai/c/${id}`; } function toastWriteUnavailable(): void { if (typeof uni === 'undefined' || typeof uni.showToast !== 'function') { return; } uni.showToast({ title: resolveScanText('writeUnavailable'), icon: 'none', }); } function toastWriteFailed(): void { if (typeof uni === 'undefined' || typeof uni.showToast !== 'function') { return; } uni.showToast({ title: resolveScanText('writeFailed'), icon: 'none', }); } function toastWriteCancelled(): void { if (typeof uni === 'undefined' || typeof uni.showToast !== 'function') { return; } uni.showToast({ title: resolveScanText('writeCancelled'), icon: 'none', }); } function isWriteCancelledError(err: { errMsg?: string } | null | undefined): boolean { if (err == null || err.errMsg == null) { return false; } const msg = String(err.errMsg).toLowerCase(); return msg.indexOf('cancel') >= 0; } /** * 贴卡写入 NDEF:仅系统写入接口的 success 回调才 toast 已写入。 * 天线未写 / 取消 / 失败 / 无可写内容均不得报写入成功。 */ export function writeNdefToTag(uri: string | null | undefined): Promise { const payload = uri == null ? '' : String(uri).trim(); if (payload === '') { toastWriteUnavailable(); return Promise.resolve(false); } return new Promise(resolve => { try { const adapter = readNfcAdapter(); if (adapter == null) { toastWriteFailed(); resolve(false); return; } const ndef = adapter.getNdef(); if (ndef == null) { toastWriteFailed(); resolve(false); return; } const runWrite = (): void => { ndef.writeNdefMessage({ uris: [payload], success: () => { uni.showToast({ title: resolveScanText('writeSuccess'), icon: 'none', }); resolve(true); }, fail: (err: { errMsg?: string }) => { if (isWriteCancelledError(err)) { toastWriteCancelled(); } else { toastWriteFailed(); } resolve(false); }, }); }; if (typeof ndef.connect === 'function') { ndef.connect({ success: () => { runWrite(); }, fail: (err: { errMsg?: string }) => { if (isWriteCancelledError(err)) { toastWriteCancelled(); } else { toastWriteFailed(); } resolve(false); }, }); return; } runWrite(); } catch (_err) { toastWriteFailed(); resolve(false); } }); }