@jolibox/implement 1.2.3 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/package-deps_build.json +13 -10
- package/.rush/temp/shrinkwrap-deps.json +2 -1
- package/dist/common/context/index.d.ts +5 -0
- package/dist/index.js +4 -8
- package/dist/index.native.js +32 -36
- package/dist/native/api/index.d.ts +1 -0
- package/dist/native/api/payment.d.ts +1 -0
- package/dist/native/payment/__tests__/payment-service-simple.test.d.ts +1 -0
- package/dist/native/payment/payment-helper.d.ts +8 -5
- package/dist/native/payment/payment-service.d.ts +38 -0
- package/implement.build.log +2 -2
- package/package.json +6 -5
- package/src/common/context/index.ts +12 -0
- package/src/native/api/call-host-method.ts +1 -1
- package/src/native/api/index.ts +1 -0
- package/src/native/api/navigate.ts +10 -1
- package/src/native/api/payment.ts +56 -0
- package/src/native/payment/__tests__/payment-service-simple.test.ts +208 -0
- package/src/native/payment/payment-helper.ts +10 -4
- package/src/native/payment/payment-service.ts +279 -0
- package/src/native/payment/registers/jolicoin-iap.ts +4 -4
- package/src/native/report/index.ts +4 -1
- package/src/native/rewards/ui/payment-modal.ts +11 -58
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { IPaymentChoice } from '@/common/rewards/reward-emitter';
|
|
2
|
+
import { paymentHelper } from './index';
|
|
3
|
+
import { isUndefinedOrNull } from '@jolibox/common';
|
|
4
|
+
import { StandardResponse } from '@jolibox/types';
|
|
5
|
+
import { applyNative } from '@jolibox/native-bridge';
|
|
6
|
+
import { innerFetch as fetch } from '@/native/network';
|
|
7
|
+
import type { PaymentResult } from './payment-helper';
|
|
8
|
+
|
|
9
|
+
type PaymentPurchaseType = 'JOLI_COIN' | 'JOLI_GEM';
|
|
10
|
+
|
|
11
|
+
const CoinFetchUrlMap: Record<PaymentPurchaseType, string> = {
|
|
12
|
+
JOLI_COIN: '/api/joli-coin/balance-detail',
|
|
13
|
+
JOLI_GEM: '/api/joli-gem/balance-detail'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// 缓存数据接口
|
|
17
|
+
export interface CachedPaymentChoices {
|
|
18
|
+
choices: IPaymentChoice[];
|
|
19
|
+
timestamp: number;
|
|
20
|
+
expiresAt: number;
|
|
21
|
+
productIds: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class PaymentService {
|
|
25
|
+
private productInfoCache: Record<PaymentPurchaseType, IPaymentChoice[]> = {
|
|
26
|
+
JOLI_COIN: [],
|
|
27
|
+
JOLI_GEM: []
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// 新增:paymentChoices 缓存
|
|
31
|
+
private static paymentChoicesCache = new Map<string, CachedPaymentChoices>();
|
|
32
|
+
private static readonly PAYMENT_CHOICES_CACHE_DURATION = 10 * 60 * 1000; // 10分钟缓存
|
|
33
|
+
|
|
34
|
+
async getJolicoinProductsInfo(type: PaymentPurchaseType) {
|
|
35
|
+
if (this.productInfoCache[type].length > 0) {
|
|
36
|
+
return this.productInfoCache[type];
|
|
37
|
+
}
|
|
38
|
+
const productsInfo = await PaymentService.getProductsInfo(CoinFetchUrlMap[type]);
|
|
39
|
+
this.productInfoCache[type] = productsInfo?.paymentChoices ?? [];
|
|
40
|
+
return this.productInfoCache[type];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async purchase(type: PaymentPurchaseType, productId: string) {
|
|
44
|
+
const productsInfo = await this.getJolicoinProductsInfo(type);
|
|
45
|
+
const appStoreProductId = productsInfo.find(
|
|
46
|
+
(choice) => choice.productId === productId
|
|
47
|
+
)?.appStoreProductId;
|
|
48
|
+
if (!appStoreProductId) {
|
|
49
|
+
throw new Error('appStoreProductId not found');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = (await paymentHelper.invokePayment(
|
|
53
|
+
type === 'JOLI_COIN' ? 'JOLI_COIN_IAP' : 'JOLI_GEM_IAP',
|
|
54
|
+
{
|
|
55
|
+
productId,
|
|
56
|
+
appStoreProductId
|
|
57
|
+
}
|
|
58
|
+
)) as PaymentResult<{ totalAmount: string }>;
|
|
59
|
+
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private static mergeResponseData(
|
|
64
|
+
responseData: { paymentChoices: IPaymentChoice[] },
|
|
65
|
+
data: { [appStoreProductId: string]: { price: string } }
|
|
66
|
+
) {
|
|
67
|
+
Object.keys(data).forEach((key) => {
|
|
68
|
+
const choice = responseData.paymentChoices.find((choice) => choice.appStoreProductId === key);
|
|
69
|
+
if (choice) {
|
|
70
|
+
choice.totalAmountStr = data[key].price;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
responseData.paymentChoices = responseData.paymentChoices.filter(
|
|
75
|
+
(choice) => !isUndefinedOrNull(choice.totalAmountStr)
|
|
76
|
+
) as IPaymentChoice[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static async getProductsInfo(apiEndpoint: string): Promise<
|
|
80
|
+
| {
|
|
81
|
+
balance: number;
|
|
82
|
+
enableAutoDeduct: boolean;
|
|
83
|
+
paymentChoices: IPaymentChoice[];
|
|
84
|
+
}
|
|
85
|
+
| undefined
|
|
86
|
+
> {
|
|
87
|
+
return Promise.race([
|
|
88
|
+
this.getProductsInfoInternal(apiEndpoint),
|
|
89
|
+
new Promise<never>((_, reject) => {
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
reject(new Error('[PaymentService] Request timeout after 3 seconds'));
|
|
92
|
+
}, 3000);
|
|
93
|
+
})
|
|
94
|
+
]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private static async getProductsInfoInternal(apiEndpoint: string): Promise<
|
|
98
|
+
| {
|
|
99
|
+
balance: number;
|
|
100
|
+
enableAutoDeduct: boolean;
|
|
101
|
+
paymentChoices: IPaymentChoice[];
|
|
102
|
+
}
|
|
103
|
+
| undefined
|
|
104
|
+
> {
|
|
105
|
+
// 首先获取服务端数据(余额等实时数据)
|
|
106
|
+
const { response } = await fetch<
|
|
107
|
+
StandardResponse<{
|
|
108
|
+
balance: number;
|
|
109
|
+
enableAutoDeduct: boolean;
|
|
110
|
+
paymentChoices: IPaymentChoice[];
|
|
111
|
+
}>
|
|
112
|
+
>(apiEndpoint, {
|
|
113
|
+
method: 'GET',
|
|
114
|
+
appendHostCookie: true,
|
|
115
|
+
responseType: 'json'
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (!response.data?.data) {
|
|
119
|
+
throw new Error('get products info failed');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const serverData = response.data.data;
|
|
123
|
+
|
|
124
|
+
// 检查 paymentChoices 缓存
|
|
125
|
+
const cachedChoices = this.getPaymentChoicesFromCache(apiEndpoint, serverData.paymentChoices);
|
|
126
|
+
|
|
127
|
+
if (cachedChoices) {
|
|
128
|
+
console.log('[PaymentService] Using cached paymentChoices');
|
|
129
|
+
|
|
130
|
+
// 使用缓存的 paymentChoices,但保持服务端的余额等实时数据
|
|
131
|
+
return {
|
|
132
|
+
balance: serverData.balance,
|
|
133
|
+
enableAutoDeduct: serverData.enableAutoDeduct,
|
|
134
|
+
paymentChoices: cachedChoices
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log('[PaymentService] Fetching fresh paymentChoices from native');
|
|
139
|
+
|
|
140
|
+
// 缓存未命中,请求原生数据
|
|
141
|
+
const { data } = await applyNative('requestProductDetailsAsync', {
|
|
142
|
+
appStoreProductIds:
|
|
143
|
+
serverData.paymentChoices
|
|
144
|
+
?.filter((choice) => typeof choice.appStoreProductId === 'string')
|
|
145
|
+
.map((choice) => choice.appStoreProductId as string) ?? []
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (data) {
|
|
149
|
+
this.mergeResponseData(serverData, data);
|
|
150
|
+
this.cachePaymentChoices(apiEndpoint, serverData.paymentChoices);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (serverData.paymentChoices.length === 0) {
|
|
154
|
+
throw new Error('paymentChoices is empty');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return serverData;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// read paymentChoices from cache
|
|
161
|
+
private static getPaymentChoicesFromCache(
|
|
162
|
+
apiEndpoint: string,
|
|
163
|
+
serverChoices: IPaymentChoice[]
|
|
164
|
+
): IPaymentChoice[] | null {
|
|
165
|
+
const cached = this.paymentChoicesCache.get(apiEndpoint);
|
|
166
|
+
|
|
167
|
+
if (!cached) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (Date.now() > cached.expiresAt) {
|
|
172
|
+
this.paymentChoicesCache.delete(apiEndpoint);
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const serverProductIds = this.extractProductIds(serverChoices);
|
|
177
|
+
const cachedProductIds = cached.productIds;
|
|
178
|
+
|
|
179
|
+
if (!this.arraysEqual(serverProductIds, cachedProductIds)) {
|
|
180
|
+
console.log('[PaymentService] Server paymentChoices changed, invalidating cache');
|
|
181
|
+
this.paymentChoicesCache.delete(apiEndpoint);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const updatedChoices = this.updateChoicesWithServerData(cached.choices, serverChoices);
|
|
186
|
+
|
|
187
|
+
return updatedChoices;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private static cachePaymentChoices(apiEndpoint: string, choices: IPaymentChoice[]): void {
|
|
191
|
+
this.paymentChoicesCache.set(apiEndpoint, {
|
|
192
|
+
choices: JSON.parse(JSON.stringify(choices)), // deep copy
|
|
193
|
+
timestamp: Date.now(),
|
|
194
|
+
expiresAt: Date.now() + this.PAYMENT_CHOICES_CACHE_DURATION,
|
|
195
|
+
productIds: this.extractProductIds(choices)
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
console.log(`[PaymentService] Cached ${choices.length} payment choices for ${apiEndpoint}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private static extractProductIds(choices: IPaymentChoice[]): string[] {
|
|
202
|
+
return choices
|
|
203
|
+
.map((choice) => choice.productId)
|
|
204
|
+
.filter(Boolean)
|
|
205
|
+
.sort();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private static arraysEqual(arr1: string[], arr2: string[]): boolean {
|
|
209
|
+
if (arr1.length !== arr2.length) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return arr1.every((item, index) => item === arr2[index]);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private static updateChoicesWithServerData(
|
|
217
|
+
cachedChoices: IPaymentChoice[],
|
|
218
|
+
serverChoices: IPaymentChoice[]
|
|
219
|
+
): IPaymentChoice[] {
|
|
220
|
+
const serverChoiceMap = new Map(serverChoices.map((choice) => [choice.productId, choice]));
|
|
221
|
+
|
|
222
|
+
return cachedChoices.map((cachedChoice) => {
|
|
223
|
+
const serverChoice = serverChoiceMap.get(cachedChoice.productId);
|
|
224
|
+
|
|
225
|
+
if (serverChoice) {
|
|
226
|
+
return {
|
|
227
|
+
...cachedChoice,
|
|
228
|
+
quantity: serverChoice.quantity
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return cachedChoice;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// clear paymentChoices cache
|
|
237
|
+
public static clearPaymentChoicesCache(apiEndpoint?: string): void {
|
|
238
|
+
if (apiEndpoint) {
|
|
239
|
+
this.paymentChoicesCache.delete(apiEndpoint);
|
|
240
|
+
console.log(`[PaymentService] Cleared cache for ${apiEndpoint}`);
|
|
241
|
+
} else {
|
|
242
|
+
this.paymentChoicesCache.clear();
|
|
243
|
+
console.log('[PaymentService] Cleared all payment choices cache');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
public static getPaymentChoicesCacheStats(): {
|
|
248
|
+
cacheCount: number;
|
|
249
|
+
validCount: number;
|
|
250
|
+
expiredCount: number;
|
|
251
|
+
} {
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
let validCount = 0;
|
|
254
|
+
let expiredCount = 0;
|
|
255
|
+
|
|
256
|
+
for (const cached of this.paymentChoicesCache.values()) {
|
|
257
|
+
if (now > cached.expiresAt) {
|
|
258
|
+
expiredCount++;
|
|
259
|
+
} else {
|
|
260
|
+
validCount++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
cacheCount: this.paymentChoicesCache.size,
|
|
266
|
+
validCount,
|
|
267
|
+
expiredCount
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
public static clearExpiredPaymentChoicesCache(): void {
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
for (const [key, cached] of this.paymentChoicesCache) {
|
|
274
|
+
if (now > cached.expiresAt) {
|
|
275
|
+
this.paymentChoicesCache.delete(key);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -75,7 +75,7 @@ onNative('onPaymentStateChange', (data) => {
|
|
|
75
75
|
pendingPayments.delete(orderUUID);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
const
|
|
78
|
+
const purchaseGem = async (params: { appStoreProductId: string; appAccountToken?: string }) => {
|
|
79
79
|
const deferred = new Deferred<StandardResponse<{ totalAmount: string }>>();
|
|
80
80
|
let targetOrderUUID: string | undefined;
|
|
81
81
|
|
|
@@ -88,7 +88,7 @@ const payInApp = async (params: { appStoreProductId: string; appAccountToken?: s
|
|
|
88
88
|
});
|
|
89
89
|
targetOrderUUID = response.data?.orderUUID;
|
|
90
90
|
|
|
91
|
-
console.info('---
|
|
91
|
+
console.info('---purchaseGem---', response);
|
|
92
92
|
if (!targetOrderUUID) {
|
|
93
93
|
throw createPaymentInternalError(
|
|
94
94
|
'orderUUID is null',
|
|
@@ -134,7 +134,7 @@ class JolicoinIAPAndroidPaymentResiter extends BasePaymentRegister<
|
|
|
134
134
|
};
|
|
135
135
|
|
|
136
136
|
pay = async (): Promise<StandardResponse<{ totalAmount: string }>> => {
|
|
137
|
-
return await
|
|
137
|
+
return await purchaseGem({ appStoreProductId: this.appStoreProductId });
|
|
138
138
|
};
|
|
139
139
|
}
|
|
140
140
|
|
|
@@ -180,7 +180,7 @@ class JolicoinIAPIOSPaymentResiter extends BasePaymentRegister<
|
|
|
180
180
|
};
|
|
181
181
|
|
|
182
182
|
pay = async (): Promise<StandardResponse<{ totalAmount: string }>> => {
|
|
183
|
-
return await
|
|
183
|
+
return await purchaseGem({
|
|
184
184
|
appStoreProductId: this.appStoreProductId,
|
|
185
185
|
appAccountToken: this.appAccountToken
|
|
186
186
|
});
|
|
@@ -13,12 +13,15 @@ const reportNative: ReportHandler = (event, data, webviewId) => {
|
|
|
13
13
|
: isString(_data.extra)
|
|
14
14
|
? JSON.parse(_data.extra)
|
|
15
15
|
: {};
|
|
16
|
+
const { mpName, mpVersion } = context.mpInfo ?? {}; //
|
|
16
17
|
const extra = {
|
|
17
18
|
...originExtra,
|
|
18
19
|
mp_id: (_data.mp_id as string) ?? '',
|
|
19
20
|
mp_version: (_data.mp_version as string) ?? '',
|
|
20
21
|
session_id: context.sessionId,
|
|
21
|
-
user_id: context.hostUserInfo?.uid ?? ''
|
|
22
|
+
user_id: context.hostUserInfo?.uid ?? '',
|
|
23
|
+
...(mpName ? { mp_name: mpName } : {}),
|
|
24
|
+
...(mpVersion ? { mp_info_version: mpVersion } : {})
|
|
22
25
|
};
|
|
23
26
|
const eventType = (_data.eventType ?? EventType.Other) as number;
|
|
24
27
|
|
|
@@ -23,12 +23,12 @@ import {
|
|
|
23
23
|
import { paymentHelper } from '@/native/payment';
|
|
24
24
|
import { createLoading } from '@jolibox/ui';
|
|
25
25
|
import { canIUseNative } from '@/native/api/base';
|
|
26
|
-
|
|
27
|
-
import { isUndefinedOrNull } from '@jolibox/common';
|
|
26
|
+
|
|
28
27
|
import { track } from '@/native/report';
|
|
29
28
|
import { updateAutoDeductConfig } from './utils';
|
|
30
29
|
import { createEventPromiseHandler } from '@/common/rewards/registers/utils/event-listener';
|
|
31
30
|
import { TrackEvent } from '@jolibox/types';
|
|
31
|
+
import { PaymentService } from '@/native/payment/payment-service';
|
|
32
32
|
|
|
33
33
|
// 货币配置映射
|
|
34
34
|
interface CurrencyPaymentConfig {
|
|
@@ -134,7 +134,7 @@ rewardsEmitter.on(
|
|
|
134
134
|
await loading.show({
|
|
135
135
|
duration: 3000
|
|
136
136
|
});
|
|
137
|
-
const balenceDetails = await
|
|
137
|
+
const balenceDetails = await PaymentService.getProductsInfo(paymentConfig.apiEndpoint);
|
|
138
138
|
loading.hide();
|
|
139
139
|
|
|
140
140
|
if (!balenceDetails) {
|
|
@@ -183,7 +183,7 @@ rewardsEmitter.on(
|
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
const balenceDetails = await
|
|
186
|
+
const balenceDetails = await PaymentService.getProductsInfo(paymentConfig.apiEndpoint);
|
|
187
187
|
if ((balenceDetails?.balance ?? 0) >= params.quantity) {
|
|
188
188
|
rewardsEmitter.emit(PaymentResultEventName, {
|
|
189
189
|
paymentResult: 'SUCCESS',
|
|
@@ -278,58 +278,11 @@ rewardsEmitter.on(
|
|
|
278
278
|
}
|
|
279
279
|
);
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
responseData: { paymentChoices: IPaymentChoice[] },
|
|
283
|
-
data: { [appStoreProductId: string]: { price: string } }
|
|
284
|
-
) => {
|
|
285
|
-
Object.keys(data).forEach((key) => {
|
|
286
|
-
const choice = responseData.paymentChoices.find((choice) => choice.appStoreProductId === key);
|
|
287
|
-
if (choice) {
|
|
288
|
-
choice.totalAmountStr = data[key].price;
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
responseData.paymentChoices = responseData.paymentChoices.filter(
|
|
293
|
-
(choice) => !isUndefinedOrNull(choice.totalAmountStr)
|
|
294
|
-
) as IPaymentChoice[];
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
const getBalenceDetails = async (
|
|
298
|
-
apiEndpoint: string
|
|
299
|
-
): Promise<
|
|
300
|
-
| {
|
|
301
|
-
balance: number;
|
|
302
|
-
enableAutoDeduct: boolean;
|
|
303
|
-
paymentChoices: IPaymentChoice[];
|
|
304
|
-
}
|
|
305
|
-
| undefined
|
|
306
|
-
> => {
|
|
307
|
-
const { response } = await fetch<
|
|
308
|
-
StandardResponse<{
|
|
309
|
-
balance: number;
|
|
310
|
-
enableAutoDeduct: boolean;
|
|
311
|
-
paymentChoices: IPaymentChoice[];
|
|
312
|
-
}>
|
|
313
|
-
>(apiEndpoint, {
|
|
314
|
-
method: 'GET',
|
|
315
|
-
appendHostCookie: true,
|
|
316
|
-
responseType: 'json'
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
console.info('getBalenceDetails', response);
|
|
320
|
-
const { data } = await applyNative('requestProductDetailsAsync', {
|
|
321
|
-
appStoreProductIds:
|
|
322
|
-
response.data?.data?.paymentChoices
|
|
323
|
-
?.filter((choice) => typeof choice.appStoreProductId === 'string')
|
|
324
|
-
.map((choice) => choice.appStoreProductId as string) ?? []
|
|
325
|
-
});
|
|
281
|
+
/** preload payment details */
|
|
326
282
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
return response.data?.data;
|
|
335
|
-
};
|
|
283
|
+
PaymentService.getProductsInfo(CURRENCY_PAYMENT_CONFIG.JOLI_COIN.apiEndpoint).catch((e) => {
|
|
284
|
+
console.error('preload joli coin payment details failed', e);
|
|
285
|
+
});
|
|
286
|
+
PaymentService.getProductsInfo(CURRENCY_PAYMENT_CONFIG.JOLI_GEM.apiEndpoint).catch((e) => {
|
|
287
|
+
console.error('preload joli gem payment details failed', e);
|
|
288
|
+
});
|