@mpgd/adapter-ait 0.6.0 → 0.8.0
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/dist/host.d.ts +49 -3
- package/dist/host.js +346 -4
- package/dist/index.js +8 -0
- package/package.json +5 -5
package/dist/host.d.ts
CHANGED
|
@@ -1,22 +1,68 @@
|
|
|
1
|
-
import { getTossShareLink, isMinVersionSupported, loadFullScreenAd, openGameCenterLeaderboard, share, showFullScreenAd, Storage, submitGameCenterLeaderBoardScore } from '@apps-in-toss/web-framework';
|
|
2
|
-
import type { ShareResult } from '@mpgd/platform';
|
|
1
|
+
import { getTossShareLink, grantPromotionRewardForGame as grantPromotionReward, isMinVersionSupported, loadFullScreenAd, openGameCenterLeaderboard, requestNotificationAgreement, share, showFullScreenAd, Storage, submitGameCenterLeaderBoardScore } from '@apps-in-toss/web-framework';
|
|
2
|
+
import type { NotificationTopic, ShareResult } from '@mpgd/platform';
|
|
3
3
|
import type { GamePlatformBridge } from './index.js';
|
|
4
4
|
export type AitIdentityProvider = () => Promise<unknown>;
|
|
5
5
|
export interface AitHostDependencies {
|
|
6
6
|
readonly identityProvider: AitIdentityProvider;
|
|
7
|
-
readonly storage: Pick<typeof Storage, 'getItem' | 'setItem'>;
|
|
7
|
+
readonly storage: Pick<typeof Storage, 'getItem' | 'removeItem' | 'setItem'>;
|
|
8
8
|
readonly getTossShareLink: typeof getTossShareLink;
|
|
9
9
|
readonly share: typeof share;
|
|
10
|
+
readonly grantPromotionReward: typeof grantPromotionReward;
|
|
11
|
+
readonly requestNotificationAgreement: typeof requestNotificationAgreement;
|
|
10
12
|
readonly isMinVersionSupported: typeof isMinVersionSupported;
|
|
11
13
|
readonly loadFullScreenAd: typeof loadFullScreenAd;
|
|
12
14
|
readonly showFullScreenAd: typeof showFullScreenAd;
|
|
13
15
|
readonly openGameCenterLeaderboard: typeof openGameCenterLeaderboard;
|
|
14
16
|
readonly submitGameCenterLeaderBoardScore: typeof submitGameCenterLeaderBoardScore;
|
|
15
17
|
}
|
|
18
|
+
export interface AitPromotionRewardConfig {
|
|
19
|
+
readonly promotionCode: string;
|
|
20
|
+
readonly amount: number;
|
|
21
|
+
}
|
|
22
|
+
export interface AitPendingPromotionGrantInput {
|
|
23
|
+
readonly campaignId: string;
|
|
24
|
+
readonly idempotencyKey: string;
|
|
25
|
+
/** Missing for legacy or malformed pending markers; resolvers must fail closed. */
|
|
26
|
+
readonly pendingSince?: string;
|
|
27
|
+
}
|
|
28
|
+
export type AitPendingPromotionGrantResolution = {
|
|
29
|
+
readonly status: 'granted';
|
|
30
|
+
readonly receiptKey: string;
|
|
31
|
+
} | {
|
|
32
|
+
readonly status: 'retry' | 'pending';
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Server-backed reconciliation hook for an ambiguous native promotion call.
|
|
36
|
+
* Returning `retry` must only happen after the game authority decides that a
|
|
37
|
+
* second provider call is safe for this single-use claim id.
|
|
38
|
+
*/
|
|
39
|
+
export type AitPendingPromotionGrantResolver = (input: AitPendingPromotionGrantInput) => Promise<AitPendingPromotionGrantResolution>;
|
|
40
|
+
export interface AitPromotionGrantAuthorizationInput {
|
|
41
|
+
readonly campaignId: string;
|
|
42
|
+
/** Single-use claim id issued by the game backend. */
|
|
43
|
+
readonly idempotencyKey: string;
|
|
44
|
+
}
|
|
45
|
+
export type AitPromotionGrantAuthorization = {
|
|
46
|
+
readonly status: 'authorized';
|
|
47
|
+
} | {
|
|
48
|
+
readonly status: 'pending' | 'rejected';
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Server-backed gate for the first provider call. A configured campaign is not
|
|
52
|
+
* available until this hook confirms that the idempotency key is a genuine,
|
|
53
|
+
* single-use game claim.
|
|
54
|
+
*/
|
|
55
|
+
export type AitPromotionGrantAuthorizer = (input: AitPromotionGrantAuthorizationInput) => Promise<AitPromotionGrantAuthorization>;
|
|
16
56
|
export interface InstallAitHostBridgeOptions {
|
|
17
57
|
readonly appName?: string;
|
|
18
58
|
readonly adGroupIds?: Readonly<Record<string, string>>;
|
|
19
59
|
readonly adPlacementTypes?: Readonly<Record<string, 'rewarded' | 'interstitial'>>;
|
|
60
|
+
/** Logical campaign ids mapped to console-issued Apps in Toss promotion configuration. */
|
|
61
|
+
readonly promotionRewards?: Readonly<Record<string, AitPromotionRewardConfig>>;
|
|
62
|
+
readonly authorizePromotionGrant?: AitPromotionGrantAuthorizer;
|
|
63
|
+
readonly resolvePendingPromotionGrant?: AitPendingPromotionGrantResolver;
|
|
64
|
+
/** Logical notification topics mapped to approved Apps in Toss template codes. */
|
|
65
|
+
readonly notificationTemplateCodes?: Partial<Readonly<Record<NotificationTopic, string>>>;
|
|
20
66
|
/** Maximum total wait for the native show request before display is observed. */
|
|
21
67
|
readonly adTimeoutMs?: number;
|
|
22
68
|
/** Maximum wait to acquire the process-wide native full-screen load slot. */
|
package/dist/host.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getTossShareLink, getUserKeyForGame, isMinVersionSupported, loadFullScreenAd, openGameCenterLeaderboard, share, showFullScreenAd, Storage, submitGameCenterLeaderBoardScore, } from '@apps-in-toss/web-framework';
|
|
1
|
+
import { getTossShareLink, getUserKeyForGame, grantPromotionRewardForGame as grantPromotionReward, isMinVersionSupported, loadFullScreenAd, openGameCenterLeaderboard, requestNotificationAgreement, share, showFullScreenAd, Storage, submitGameCenterLeaderBoardScore, } from '@apps-in-toss/web-framework';
|
|
2
2
|
import { assertBridgeRequest, bridgeStorageLoadProtocol, } from '@mpgd/bridge';
|
|
3
3
|
import { dispatchAitLifecycleEvent } from './lifecycle.js';
|
|
4
4
|
const defaultAdTimeoutMs = 60000;
|
|
@@ -7,6 +7,14 @@ const defaultAdDisplayStartTimeoutMs = 60000;
|
|
|
7
7
|
const defaultAdMaximumDisplayMs = 30 * 60000;
|
|
8
8
|
const invalidBridgeRequestId = 'ait-invalid-request';
|
|
9
9
|
const rewardedAdEvidenceSchema = 'apps-in-toss.rewarded-ad.callback.v1';
|
|
10
|
+
const minimumPromotionTossAppVersion = '5.232.0';
|
|
11
|
+
const promotionGrantStoragePrefix = 'mpgd:ait:promotion-grant:v1:';
|
|
12
|
+
const defaultNotificationAgreementTimeoutMs = 120000;
|
|
13
|
+
const notificationTopics = new Set([
|
|
14
|
+
'daily-ready',
|
|
15
|
+
'streak-at-risk',
|
|
16
|
+
'friend-challenge',
|
|
17
|
+
]);
|
|
10
18
|
const launchEntries = new Set([
|
|
11
19
|
'home',
|
|
12
20
|
'daily',
|
|
@@ -21,6 +29,8 @@ const defaultDependencies = {
|
|
|
21
29
|
storage: Storage,
|
|
22
30
|
getTossShareLink,
|
|
23
31
|
share,
|
|
32
|
+
grantPromotionReward,
|
|
33
|
+
requestNotificationAgreement,
|
|
24
34
|
isMinVersionSupported,
|
|
25
35
|
loadFullScreenAd,
|
|
26
36
|
showFullScreenAd,
|
|
@@ -37,6 +47,12 @@ export function createAitHostBridge(options = {}) {
|
|
|
37
47
|
const appName = normalizeAppName(options.appName ?? 'mpgd-kit');
|
|
38
48
|
const adGroupIds = normalizeAdGroupIds(options.adGroupIds);
|
|
39
49
|
const adPlacementTypes = normalizeAdPlacementTypes(options.adPlacementTypes);
|
|
50
|
+
const promotionRewards = normalizePromotionRewards(options.promotionRewards);
|
|
51
|
+
const authorizePromotionGrant = options.authorizePromotionGrant;
|
|
52
|
+
const resolvePendingPromotionGrant = options.resolvePendingPromotionGrant;
|
|
53
|
+
const notificationTemplateCodes = normalizeNotificationTemplateCodes(options.notificationTemplateCodes);
|
|
54
|
+
const promotionGrantsInFlight = new Map();
|
|
55
|
+
const notificationSubscriptions = new Set();
|
|
40
56
|
const loadedAdGroupIds = new Set();
|
|
41
57
|
const loadingAdGroups = new Map();
|
|
42
58
|
const activeAdGroupIds = new Set();
|
|
@@ -111,9 +127,67 @@ export function createAitHostBridge(options = {}) {
|
|
|
111
127
|
case 'share.readInboundShare':
|
|
112
128
|
return ok(request, readInboundShare());
|
|
113
129
|
case 'notifications.getStatus':
|
|
114
|
-
return ok(request,
|
|
115
|
-
case 'notifications.requestSubscription':
|
|
116
|
-
|
|
130
|
+
return ok(request, getNotificationStatus(readNotificationTopic(request.payload), notificationTemplateCodes, notificationSubscriptions, dependencies));
|
|
131
|
+
case 'notifications.requestSubscription': {
|
|
132
|
+
const topic = readNotificationTopic(request.payload);
|
|
133
|
+
const templateCode = notificationTemplateCodes.get(topic);
|
|
134
|
+
if (templateCode === undefined
|
|
135
|
+
|| !isCapabilitySupported(() => dependencies.requestNotificationAgreement.isSupported())) {
|
|
136
|
+
return ok(request, 'unavailable');
|
|
137
|
+
}
|
|
138
|
+
const result = await requestAitNotificationAgreement(dependencies.requestNotificationAgreement, templateCode);
|
|
139
|
+
if (result === 'subscribed') {
|
|
140
|
+
notificationSubscriptions.add(topic);
|
|
141
|
+
}
|
|
142
|
+
return ok(request, result);
|
|
143
|
+
}
|
|
144
|
+
case 'promotions.getAvailability': {
|
|
145
|
+
const campaignId = readCampaignId(request.payload);
|
|
146
|
+
const availability = getPromotionAvailability(campaignId, promotionRewards, authorizePromotionGrant, dependencies);
|
|
147
|
+
return ok(request, availability);
|
|
148
|
+
}
|
|
149
|
+
case 'promotions.grantReward': {
|
|
150
|
+
const campaignId = readCampaignId(request.payload);
|
|
151
|
+
const reward = promotionRewards.get(campaignId);
|
|
152
|
+
const idempotencyKey = readRequiredIdempotencyKey(request.payload);
|
|
153
|
+
if (reward === undefined
|
|
154
|
+
|| !isPromotionSupported(dependencies)) {
|
|
155
|
+
return ok(request, { status: 'unavailable' });
|
|
156
|
+
}
|
|
157
|
+
const persisted = await resolvePersistedPromotionGrant({
|
|
158
|
+
campaignId,
|
|
159
|
+
idempotencyKey,
|
|
160
|
+
storage: dependencies.storage,
|
|
161
|
+
...(resolvePendingPromotionGrant === undefined
|
|
162
|
+
? {}
|
|
163
|
+
: { resolver: resolvePendingPromotionGrant }),
|
|
164
|
+
});
|
|
165
|
+
if (persisted.result !== undefined) {
|
|
166
|
+
return ok(request, persisted.result);
|
|
167
|
+
}
|
|
168
|
+
const existing = promotionGrantsInFlight.get(idempotencyKey);
|
|
169
|
+
if (existing !== undefined) {
|
|
170
|
+
return ok(request, await existing);
|
|
171
|
+
}
|
|
172
|
+
const pending = authorizeAndGrantAitPromotionReward({
|
|
173
|
+
campaignId,
|
|
174
|
+
idempotencyKey,
|
|
175
|
+
reward,
|
|
176
|
+
dependencies,
|
|
177
|
+
...(authorizePromotionGrant === undefined
|
|
178
|
+
? {}
|
|
179
|
+
: { authorizer: authorizePromotionGrant }),
|
|
180
|
+
});
|
|
181
|
+
promotionGrantsInFlight.set(idempotencyKey, pending);
|
|
182
|
+
try {
|
|
183
|
+
return ok(request, await pending);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
if (promotionGrantsInFlight.get(idempotencyKey) === pending) {
|
|
187
|
+
promotionGrantsInFlight.delete(idempotencyKey);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
117
191
|
// IAP must be installed with game-owned server verification. Never return demo grants.
|
|
118
192
|
case 'commerce.getProducts':
|
|
119
193
|
return ok(request, []);
|
|
@@ -228,6 +302,227 @@ export function createAitHostBridge(options = {}) {
|
|
|
228
302
|
}
|
|
229
303
|
}
|
|
230
304
|
}
|
|
305
|
+
function getNotificationStatus(topic, templateCodes, subscriptions, dependencies) {
|
|
306
|
+
if (!templateCodes.has(topic)) {
|
|
307
|
+
return 'configuration-required';
|
|
308
|
+
}
|
|
309
|
+
if (!isCapabilitySupported(() => dependencies.requestNotificationAgreement.isSupported())) {
|
|
310
|
+
return 'unsupported';
|
|
311
|
+
}
|
|
312
|
+
return subscriptions.has(topic) ? 'subscribed' : 'not-subscribed';
|
|
313
|
+
}
|
|
314
|
+
function requestAitNotificationAgreement(requestAgreement, templateCode) {
|
|
315
|
+
return new Promise((resolve) => {
|
|
316
|
+
let settled = false;
|
|
317
|
+
let cleanup = () => { };
|
|
318
|
+
const finish = (result) => {
|
|
319
|
+
if (settled) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
settled = true;
|
|
323
|
+
globalThis.clearTimeout(timer);
|
|
324
|
+
cleanup();
|
|
325
|
+
resolve(result);
|
|
326
|
+
};
|
|
327
|
+
const timer = globalThis.setTimeout(() => finish('unavailable'), defaultNotificationAgreementTimeoutMs);
|
|
328
|
+
try {
|
|
329
|
+
const unregister = requestAgreement({
|
|
330
|
+
options: { templateCode },
|
|
331
|
+
onEvent: ({ type }) => {
|
|
332
|
+
finish(type === 'agreementRejected' ? 'rejected' : 'subscribed');
|
|
333
|
+
},
|
|
334
|
+
onError: () => finish('unavailable'),
|
|
335
|
+
});
|
|
336
|
+
cleanup = unregister;
|
|
337
|
+
if (settled) {
|
|
338
|
+
cleanup();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
finish('unavailable');
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
function getPromotionAvailability(campaignId, rewards, authorizer, dependencies) {
|
|
347
|
+
if (!rewards.has(campaignId) || authorizer === undefined) {
|
|
348
|
+
return 'configuration-required';
|
|
349
|
+
}
|
|
350
|
+
return isPromotionSupported(dependencies) ? 'available' : 'unsupported';
|
|
351
|
+
}
|
|
352
|
+
async function authorizeAndGrantAitPromotionReward(input) {
|
|
353
|
+
if (input.authorizer === undefined) {
|
|
354
|
+
return { status: 'unavailable' };
|
|
355
|
+
}
|
|
356
|
+
let authorization;
|
|
357
|
+
try {
|
|
358
|
+
authorization = await input.authorizer({
|
|
359
|
+
campaignId: input.campaignId,
|
|
360
|
+
idempotencyKey: input.idempotencyKey,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
console.warn('AIT promotion grant authorization failed; keeping the claim pending.', input.campaignId, error);
|
|
365
|
+
return { status: 'pending' };
|
|
366
|
+
}
|
|
367
|
+
if (authorization.status !== 'authorized') {
|
|
368
|
+
return {
|
|
369
|
+
status: authorization.status === 'pending' ? 'pending' : 'unavailable',
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return grantAitPromotionReward(input.dependencies, input.reward, input.idempotencyKey);
|
|
373
|
+
}
|
|
374
|
+
function isPromotionSupported(dependencies) {
|
|
375
|
+
return isCapabilitySupported(() => dependencies.isMinVersionSupported({
|
|
376
|
+
android: minimumPromotionTossAppVersion,
|
|
377
|
+
ios: minimumPromotionTossAppVersion,
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
async function grantAitPromotionReward(dependencies, reward, idempotencyKey) {
|
|
381
|
+
const storageKey = promotionGrantStorageKey(idempotencyKey);
|
|
382
|
+
await dependencies.storage.setItem(storageKey, JSON.stringify({
|
|
383
|
+
status: 'pending',
|
|
384
|
+
pendingSince: new Date().toISOString(),
|
|
385
|
+
}));
|
|
386
|
+
try {
|
|
387
|
+
const response = await dependencies.grantPromotionReward({
|
|
388
|
+
params: {
|
|
389
|
+
promotionCode: reward.promotionCode,
|
|
390
|
+
amount: reward.amount,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
if (isExplicitPromotionFailure(response)) {
|
|
394
|
+
return clearFailedPromotionGrant(dependencies.storage, storageKey);
|
|
395
|
+
}
|
|
396
|
+
if (!isRecord(response)
|
|
397
|
+
|| typeof response.key !== 'string'
|
|
398
|
+
|| response.key.trim().length === 0) {
|
|
399
|
+
// An undocumented shape is ambiguous after native dispatch. Preserve the
|
|
400
|
+
// marker so a backend resolver can determine whether a grant happened.
|
|
401
|
+
return { status: 'pending' };
|
|
402
|
+
}
|
|
403
|
+
const result = { status: 'granted', receiptKey: response.key };
|
|
404
|
+
try {
|
|
405
|
+
await dependencies.storage.setItem(storageKey, JSON.stringify(result));
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
console.warn('AIT native promotion receipt could not be cached; keeping it terminal.', reward.promotionCode, error);
|
|
409
|
+
}
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// Keep the pending marker. A provider error after dispatch is ambiguous and
|
|
414
|
+
// retrying the same promotion blindly can double-grant Toss points.
|
|
415
|
+
return { status: 'pending' };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function isExplicitPromotionFailure(value) {
|
|
419
|
+
if (value === 'ERROR') {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
return isRecord(value)
|
|
423
|
+
&& typeof value.errorCode === 'string'
|
|
424
|
+
&& value.errorCode.trim().length > 0
|
|
425
|
+
&& typeof value.message === 'string';
|
|
426
|
+
}
|
|
427
|
+
async function clearFailedPromotionGrant(storage, storageKey) {
|
|
428
|
+
try {
|
|
429
|
+
await storage.removeItem(storageKey);
|
|
430
|
+
return { status: 'failed' };
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
433
|
+
console.warn('AIT failed promotion marker could not be cleared; keeping the claim pending.', storageKey, error);
|
|
434
|
+
return { status: 'pending' };
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
async function resolvePersistedPromotionGrant(input) {
|
|
438
|
+
const storageKey = promotionGrantStorageKey(input.idempotencyKey);
|
|
439
|
+
const serialized = await input.storage.getItem(storageKey);
|
|
440
|
+
if (serialized === null) {
|
|
441
|
+
return {};
|
|
442
|
+
}
|
|
443
|
+
let state;
|
|
444
|
+
try {
|
|
445
|
+
state = JSON.parse(serialized);
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
console.warn('AIT persisted promotion state is unreadable; keeping the claim pending.', input.campaignId, error);
|
|
449
|
+
return { result: { status: 'pending' } };
|
|
450
|
+
}
|
|
451
|
+
if (!isRecord(state)) {
|
|
452
|
+
console.warn('AIT persisted promotion state is malformed; keeping the claim pending.', input.campaignId);
|
|
453
|
+
return { result: { status: 'pending' } };
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
if (state.status === 'granted'
|
|
457
|
+
&& typeof state.receiptKey === 'string'
|
|
458
|
+
&& state.receiptKey.trim().length > 0) {
|
|
459
|
+
return { result: { status: 'granted', receiptKey: state.receiptKey } };
|
|
460
|
+
}
|
|
461
|
+
if (state.status === 'pending') {
|
|
462
|
+
const pendingSince = normalizePendingSince(state.pendingSince);
|
|
463
|
+
if (pendingSince === undefined) {
|
|
464
|
+
return { result: { status: 'pending' } };
|
|
465
|
+
}
|
|
466
|
+
if (input.resolver === undefined) {
|
|
467
|
+
return { result: { status: 'pending' } };
|
|
468
|
+
}
|
|
469
|
+
let resolution;
|
|
470
|
+
try {
|
|
471
|
+
resolution = await input.resolver({
|
|
472
|
+
campaignId: input.campaignId,
|
|
473
|
+
idempotencyKey: input.idempotencyKey,
|
|
474
|
+
pendingSince,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
catch (error) {
|
|
478
|
+
console.warn('AIT pending promotion grant resolver failed; keeping the claim pending.', input.campaignId, error);
|
|
479
|
+
return { result: { status: 'pending' } };
|
|
480
|
+
}
|
|
481
|
+
if (resolution.status === 'pending') {
|
|
482
|
+
return { result: { status: 'pending' } };
|
|
483
|
+
}
|
|
484
|
+
if (resolution.status === 'granted') {
|
|
485
|
+
if (typeof resolution.receiptKey !== 'string'
|
|
486
|
+
|| resolution.receiptKey.trim().length === 0) {
|
|
487
|
+
return { result: { status: 'pending' } };
|
|
488
|
+
}
|
|
489
|
+
const result = { status: 'granted', receiptKey: resolution.receiptKey };
|
|
490
|
+
try {
|
|
491
|
+
await input.storage.setItem(storageKey, JSON.stringify(result));
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
console.warn('AIT reconciled promotion receipt could not be cached; keeping it terminal.', input.campaignId, error);
|
|
495
|
+
}
|
|
496
|
+
return { result };
|
|
497
|
+
}
|
|
498
|
+
return removeInvalidPersistedPromotionGrant(input.storage, storageKey);
|
|
499
|
+
}
|
|
500
|
+
console.warn('AIT persisted promotion state has an unknown status; keeping the claim pending.', input.campaignId);
|
|
501
|
+
return { result: { status: 'pending' } };
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
console.warn('AIT persisted promotion state could not be reconciled; keeping the claim pending.', input.campaignId, error);
|
|
505
|
+
return { result: { status: 'pending' } };
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function removeInvalidPersistedPromotionGrant(storage, storageKey) {
|
|
509
|
+
try {
|
|
510
|
+
await storage.removeItem(storageKey);
|
|
511
|
+
return {};
|
|
512
|
+
}
|
|
513
|
+
catch {
|
|
514
|
+
return { result: { status: 'pending' } };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function promotionGrantStorageKey(idempotencyKey) {
|
|
518
|
+
return `${promotionGrantStoragePrefix}${encodeURIComponent(idempotencyKey)}`;
|
|
519
|
+
}
|
|
520
|
+
function normalizePendingSince(value) {
|
|
521
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
522
|
+
return undefined;
|
|
523
|
+
}
|
|
524
|
+
return Number.isFinite(Date.parse(value)) ? value : undefined;
|
|
525
|
+
}
|
|
231
526
|
export async function shareIntent(payload, dependencies) {
|
|
232
527
|
const intent = readPayloadRecord(payload);
|
|
233
528
|
if (typeof intent.text !== 'string' || typeof intent.deepLink !== 'string') {
|
|
@@ -655,6 +950,32 @@ function normalizeAdPlacementTypes(input) {
|
|
|
655
950
|
.map(([placementId, type]) => [placementId.trim(), type])
|
|
656
951
|
.filter(([placementId]) => placementId.length > 0));
|
|
657
952
|
}
|
|
953
|
+
function normalizePromotionRewards(input) {
|
|
954
|
+
const rewards = new Map();
|
|
955
|
+
for (const [rawCampaignId, rawReward] of Object.entries(input ?? {})) {
|
|
956
|
+
const campaignId = rawCampaignId.trim();
|
|
957
|
+
const promotionCode = rawReward.promotionCode.trim();
|
|
958
|
+
if (campaignId.length === 0 || promotionCode.length === 0) {
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
if (!Number.isSafeInteger(rawReward.amount) || rawReward.amount <= 0) {
|
|
962
|
+
console.warn(`AIT promotion amount must be a positive safe integer; disabling campaign: ${campaignId}`);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
rewards.set(campaignId, { promotionCode, amount: rawReward.amount });
|
|
966
|
+
}
|
|
967
|
+
return rewards;
|
|
968
|
+
}
|
|
969
|
+
function normalizeNotificationTemplateCodes(input) {
|
|
970
|
+
const templateCodes = new Map();
|
|
971
|
+
for (const topic of notificationTopics) {
|
|
972
|
+
const templateCode = input?.[topic]?.trim();
|
|
973
|
+
if (templateCode !== undefined && templateCode.length > 0) {
|
|
974
|
+
templateCodes.set(topic, templateCode);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return templateCodes;
|
|
978
|
+
}
|
|
658
979
|
function normalizeAppName(value) {
|
|
659
980
|
const normalized = value.trim();
|
|
660
981
|
if (!/^[A-Za-z0-9-]+$/u.test(normalized)) {
|
|
@@ -701,6 +1022,27 @@ function readIdempotencyKey(payload, fallback) {
|
|
|
701
1022
|
const value = readPayloadRecord(payload).idempotencyKey;
|
|
702
1023
|
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
|
703
1024
|
}
|
|
1025
|
+
function readRequiredIdempotencyKey(payload) {
|
|
1026
|
+
const value = readPayloadRecord(payload).idempotencyKey;
|
|
1027
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 256) {
|
|
1028
|
+
throw new TypeError('AIT promotion idempotencyKey must contain 1 to 256 characters.');
|
|
1029
|
+
}
|
|
1030
|
+
return value;
|
|
1031
|
+
}
|
|
1032
|
+
function readCampaignId(payload) {
|
|
1033
|
+
const value = readPayloadRecord(payload).campaignId;
|
|
1034
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 128) {
|
|
1035
|
+
throw new TypeError('AIT promotion campaignId must contain 1 to 128 characters.');
|
|
1036
|
+
}
|
|
1037
|
+
return value;
|
|
1038
|
+
}
|
|
1039
|
+
function readNotificationTopic(payload) {
|
|
1040
|
+
const value = readPayloadRecord(payload).topic;
|
|
1041
|
+
if (typeof value !== 'string' || !notificationTopics.has(value)) {
|
|
1042
|
+
throw new TypeError('AIT notification topic is invalid.');
|
|
1043
|
+
}
|
|
1044
|
+
return value;
|
|
1045
|
+
}
|
|
704
1046
|
function readFiniteScore(payload) {
|
|
705
1047
|
const score = readPayloadRecord(payload).score;
|
|
706
1048
|
if (typeof score !== 'number' || !Number.isFinite(score)) {
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,10 @@ export function createAitPlatformGateway(input) {
|
|
|
44
44
|
getStatus: (topic) => request('notifications.getStatus', { topic }),
|
|
45
45
|
requestSubscription: (topic) => request('notifications.requestSubscription', { topic }),
|
|
46
46
|
},
|
|
47
|
+
promotions: {
|
|
48
|
+
getAvailability: (payload) => request('promotions.getAvailability', payload),
|
|
49
|
+
grantReward: (payload) => request('promotions.grantReward', payload),
|
|
50
|
+
},
|
|
47
51
|
commerce: {
|
|
48
52
|
getProducts: () => request('commerce.getProducts', {}),
|
|
49
53
|
purchase: (payload) => request('commerce.purchase', payload),
|
|
@@ -132,6 +136,10 @@ export function createAitSandboxBridge(options = {}) {
|
|
|
132
136
|
return ok(input, 'configuration-required');
|
|
133
137
|
case 'notifications.requestSubscription':
|
|
134
138
|
return ok(input, 'unavailable');
|
|
139
|
+
case 'promotions.getAvailability':
|
|
140
|
+
return ok(input, 'configuration-required');
|
|
141
|
+
case 'promotions.grantReward':
|
|
142
|
+
return ok(input, { status: 'unavailable' });
|
|
135
143
|
case 'commerce.getProducts':
|
|
136
144
|
return ok(input, [
|
|
137
145
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/adapter-ait",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Apps in Toss WebView platform adapter for mpgd games.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -47,11 +47,11 @@
|
|
|
47
47
|
"dist"
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@mpgd/
|
|
51
|
-
"@mpgd/
|
|
50
|
+
"@mpgd/platform": "0.8.0",
|
|
51
|
+
"@mpgd/bridge": "0.7.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
|
-
"@apps-in-toss/web-framework": ">=
|
|
54
|
+
"@apps-in-toss/web-framework": ">=3.0.0-beta <4"
|
|
55
55
|
},
|
|
56
56
|
"peerDependenciesMeta": {
|
|
57
57
|
"@apps-in-toss/web-framework": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@apps-in-toss/web-framework": "
|
|
62
|
+
"@apps-in-toss/web-framework": "3.0.0-beta.da14818",
|
|
63
63
|
"ttsc": "0.18.4",
|
|
64
64
|
"typescript": "7.0.2",
|
|
65
65
|
"vitest": "^4.0.0"
|