Repository navigation
Expand file tree
/
Copy pathmobileTapdb.ts
More file actions
152 lines (134 loc) · 5.13 KB
/
Copy pathmobileTapdb.ts
File metadata and controls
152 lines (134 loc) · 5.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
import Constants from 'expo-constants';
import { Platform } from 'react-native';
import {
clearTapdbUser as clearNativeTapdbUser,
initializeTapdb,
setTapdbUserId,
type TapdbRegion,
} from 'xdt-tapdb';
import { hydrateAnalyticsConsent, isAnalyticsAllowed } from './analyticsConsentStore';
export type MobileTapdbInitStatus =
| { ok: true }
| {
ok: false;
reason: 'not_consented' | 'missing_config' | 'native_unavailable' | 'native_error';
};
let initPromise: Promise<MobileTapdbInitStatus> | null = null;
/** 自建线通过 Expo extra 烘焙的 TapDB 公开配置。 */
interface SelfHostTapdbConfig {
clientId?: string;
clientToken?: string;
region?: string;
channel?: string;
}
function selfHostTapdbConfig(): SelfHostTapdbConfig {
const cindy = Constants.expoConfig?.extra?.cindy as
| { tapdb?: SelfHostTapdbConfig }
| undefined;
return cindy?.tapdb ?? {};
}
/**
* 初始化 TapDB —— **只有用户明示同意《隐私政策》且统计开关开启时**才会真正初始化
* 原生 SDK。
*
* 原生 SDK 一旦 init 就无法反初始化(TapTap Core 没有 opt-out API),所以同意闸必须
* 挡在 init 之前,而不是靠事后停止上报。未放行时返回 `not_consented` 且**不缓存**
* 这个结果——用户在登录页同意后会再调一次,那一次才真正 init。
*/
export function initMobileTapdb(): Promise<MobileTapdbInitStatus> {
if (initPromise) return initPromise;
return hydrateAnalyticsConsent().then(() => {
if (initPromise) return initPromise;
if (!isAnalyticsAllowed()) {
return { ok: false as const, reason: 'not_consented' as const };
}
return startInit();
});
}
function startInit(): Promise<MobileTapdbInitStatus> {
const selfHostConfig = selfHostTapdbConfig();
const clientId =
selfHostConfig.clientId?.trim() ||
process.env.EXPO_PUBLIC_TAPTAP_CLIENT_ID?.trim();
const clientToken =
selfHostConfig.clientToken?.trim() ||
process.env.EXPO_PUBLIC_TAPTAP_CLIENT_TOKEN?.trim();
if (!clientId || !clientToken) {
initPromise = Promise.resolve({ ok: false, reason: 'missing_config' });
return initPromise;
}
initPromise = initializeTapdb({
clientId,
clientToken,
region: resolveTapdbRegion(
selfHostConfig.region || process.env.EXPO_PUBLIC_TAPDB_REGION,
),
channel: resolveTapdbChannel(
selfHostConfig.channel || process.env.EXPO_PUBLIC_TAPDB_CHANNEL,
Platform.OS,
),
properties: {
xdt_surface: 'mobile',
xdt_platform: Platform.OS,
xdt_app_version: getMobileTapdbVersion(),
},
})
.then((available) => available ? { ok: true as const } : { ok: false as const, reason: 'native_unavailable' as const })
.catch(() => ({ ok: false as const, reason: 'native_error' as const }));
return initPromise;
}
/**
* 每次用到 SDK 之前都要重新问一遍同意闸,不能只信 `initPromise` 的缓存结果。
*
* 反例:账号 A 同意过 → SDK 已 init(initPromise 永久 resolved)→ 登出撤销同意 →
* 账号 B 走企业 SSO 登录(SSO 被协议门豁免,B 从没同意过)。若直接复用缓存的
* `{ ok: true }`,就会给一个从未同意的用户绑定账号标识并上报。
*/
async function ensureReportingAllowed(): Promise<boolean> {
await hydrateAnalyticsConsent();
if (!isAnalyticsAllowed()) return false;
const status = await initMobileTapdb();
return status.ok;
}
export async function setTapdbUser(userId: string): Promise<void> {
if (!(await ensureReportingAllowed())) return;
await setTapdbUserId(userId).catch(() => undefined);
}
export async function clearTapdbUser(): Promise<void> {
// 只对已经 init 的实例有意义;没 init 过就没什么可解绑的,更不该为了一次登出
// 把 SDK 拉起来。这里不用 ensureReportingAllowed:解绑是"减少"而非"开始"采集,
// 撤销同意后仍然应该允许执行。
if (!initPromise) return;
const status = await initPromise;
if (!status.ok) return;
await clearNativeTapdbUser().catch(() => undefined);
}
/**
* 用户在设置里关掉统计时调用:立刻解绑账号标识,并停止我们主动触发的一切上报。
*
* 原生 SDK 不支持反初始化,本次进程内已 init 的实例依然在;"完全不初始化"在下次
* 冷启动生效(那时 isAnalyticsAllowed() 已经是 false)。这也是设置页文案不承诺
* "立即抹除"的原因。
*/
export async function stopMobileTapdbReporting(): Promise<void> {
if (!initPromise) return;
const status = await initPromise;
if (!status.ok) return;
await clearNativeTapdbUser().catch(() => undefined);
}
export function resolveTapdbChannel(value: string | undefined, platform = Platform.OS): string {
const trimmed = value?.trim();
if (trimmed) return trimmed;
return platform === 'ios' ? 'AppStore' : 'NPKG';
}
export function resolveTapdbRegion(value: string | undefined): TapdbRegion {
return value?.trim().toLowerCase() === 'global' ? 'global' : 'cn';
}
export function getMobileTapdbVersion(): string {
return Constants.nativeAppVersion ?? Constants.expoConfig?.version ?? '0.0.0';
}
export const __testing = {
resetInitPromise(): void {
initPromise = null;
},
};