@mpgd/adapter-ait 0.9.0 → 0.11.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/ad-config.d.ts +2 -1
- package/dist/ad-config.js +4 -1
- package/dist/host.d.ts +17 -2
- package/dist/host.js +201 -2
- package/dist/index.js +7 -0
- package/dist/local-mock.d.ts +27 -0
- package/dist/local-mock.js +21 -0
- package/dist/safe-area.d.ts +54 -0
- package/dist/safe-area.js +132 -0
- package/package.json +8 -4
package/dist/ad-config.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
export type AitAdPlacementType = 'rewarded' | 'interstitial' | 'banner';
|
|
1
2
|
export interface AitAdBridgeConfig {
|
|
2
3
|
readonly adGroupIds: Readonly<Record<string, string>>;
|
|
3
|
-
readonly adPlacementTypes: Readonly<Record<string,
|
|
4
|
+
readonly adPlacementTypes: Readonly<Record<string, AitAdPlacementType>>;
|
|
4
5
|
}
|
|
5
6
|
export declare function extractAitAdBridgeConfig(input: unknown, sourceLabel?: string): AitAdBridgeConfig;
|
package/dist/ad-config.js
CHANGED
|
@@ -9,7 +9,7 @@ export function extractAitAdBridgeConfig(input, sourceLabel = 'ad placements') {
|
|
|
9
9
|
for (const [placementIndex, placement] of placements.entries()) {
|
|
10
10
|
if (!isRecord(placement)
|
|
11
11
|
|| typeof placement.id !== 'string'
|
|
12
|
-
|| (placement.type
|
|
12
|
+
|| !isAitAdPlacementType(placement.type)) {
|
|
13
13
|
throw new Error(`AIT ad placement entry at index ${placementIndex} is invalid: ${sourceLabel}`);
|
|
14
14
|
}
|
|
15
15
|
if (placementIds.has(placement.id)) {
|
|
@@ -28,6 +28,9 @@ export function extractAitAdBridgeConfig(input, sourceLabel = 'ad placements') {
|
|
|
28
28
|
}
|
|
29
29
|
return { adGroupIds, adPlacementTypes };
|
|
30
30
|
}
|
|
31
|
+
function isAitAdPlacementType(value) {
|
|
32
|
+
return value === 'rewarded' || value === 'interstitial' || value === 'banner';
|
|
33
|
+
}
|
|
31
34
|
function isRecord(value) {
|
|
32
35
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
33
36
|
}
|
package/dist/host.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { getTossShareLink, grantPromotionRewardForGame as grantPromotionReward, IAP, isMinVersionSupported, loadFullScreenAd, openGameCenterLeaderboard, requestNotificationAgreement, share, showFullScreenAd, Storage, submitGameCenterLeaderBoardScore } from '@apps-in-toss/web-framework';
|
|
1
|
+
import { getTossShareLink, grantPromotionRewardForGame as grantPromotionReward, IAP, isMinVersionSupported, loadFullScreenAd, openGameCenterLeaderboard, requestNotificationAgreement, share, showFullScreenAd, Storage, submitGameCenterLeaderBoardScore, TossAds } from '@apps-in-toss/web-framework';
|
|
2
2
|
import type { Entitlement, LogicalProductId, NotificationTopic, ShareResult } from '@mpgd/platform';
|
|
3
|
+
import type { AitAdPlacementType } from './ad-config.js';
|
|
3
4
|
import type { GamePlatformBridge } from './index.js';
|
|
4
5
|
export type AitIdentityProvider = () => Promise<unknown>;
|
|
5
6
|
/**
|
|
@@ -18,6 +19,10 @@ interface AitIapDependencies {
|
|
|
18
19
|
readonly getPendingOrders: AitNativeMethod<typeof IAP.getPendingOrders>;
|
|
19
20
|
readonly completeProductGrant: AitNativeMethod<typeof IAP.completeProductGrant>;
|
|
20
21
|
}
|
|
22
|
+
interface AitBannerDependencies {
|
|
23
|
+
readonly initialize: typeof TossAds.initialize;
|
|
24
|
+
readonly attachBanner: typeof TossAds.attachBanner;
|
|
25
|
+
}
|
|
21
26
|
export interface AitHostDependencies {
|
|
22
27
|
readonly identityProvider: AitIdentityProvider;
|
|
23
28
|
readonly storage: Pick<typeof Storage, 'getItem' | 'removeItem' | 'setItem'>;
|
|
@@ -28,6 +33,7 @@ export interface AitHostDependencies {
|
|
|
28
33
|
readonly isMinVersionSupported: typeof isMinVersionSupported;
|
|
29
34
|
readonly loadFullScreenAd: AitNativeMethod<typeof loadFullScreenAd>;
|
|
30
35
|
readonly showFullScreenAd: AitNativeMethod<typeof showFullScreenAd>;
|
|
36
|
+
readonly tossAds: AitBannerDependencies;
|
|
31
37
|
readonly openGameCenterLeaderboard: AitNativeMethod<typeof openGameCenterLeaderboard>;
|
|
32
38
|
readonly submitGameCenterLeaderBoardScore: AitNativeMethod<typeof submitGameCenterLeaderBoardScore>;
|
|
33
39
|
readonly iap: AitIapDependencies;
|
|
@@ -108,10 +114,15 @@ export type AitPromotionGrantAuthorization = {
|
|
|
108
114
|
* single-use game claim.
|
|
109
115
|
*/
|
|
110
116
|
export type AitPromotionGrantAuthorizer = (input: AitPromotionGrantAuthorizationInput) => Promise<AitPromotionGrantAuthorization>;
|
|
117
|
+
export interface AitBannerAppearance {
|
|
118
|
+
readonly theme?: 'auto' | 'light' | 'dark';
|
|
119
|
+
readonly tone?: 'blackAndWhite' | 'grey';
|
|
120
|
+
readonly variant?: 'card' | 'expanded';
|
|
121
|
+
}
|
|
111
122
|
export interface InstallAitHostBridgeOptions {
|
|
112
123
|
readonly appName?: string;
|
|
113
124
|
readonly adGroupIds?: Readonly<Record<string, string>>;
|
|
114
|
-
readonly adPlacementTypes?: Readonly<Record<string,
|
|
125
|
+
readonly adPlacementTypes?: Readonly<Record<string, AitAdPlacementType>>;
|
|
115
126
|
/** Logical campaign ids mapped to console-issued Apps in Toss promotion configuration. */
|
|
116
127
|
readonly promotionRewards?: Readonly<Record<string, AitPromotionRewardConfig>>;
|
|
117
128
|
readonly authorizePromotionGrant?: AitPromotionGrantAuthorizer;
|
|
@@ -134,6 +145,10 @@ export interface InstallAitHostBridgeOptions {
|
|
|
134
145
|
readonly adDisplayStartTimeoutMs?: number;
|
|
135
146
|
/** Last-resort cleanup when the native SDK omits its terminal display callback. */
|
|
136
147
|
readonly adMaximumDisplayMs?: number;
|
|
148
|
+
/** Maximum wait for an inline banner to render or report no-fill/failure. */
|
|
149
|
+
readonly bannerRenderTimeoutMs?: number;
|
|
150
|
+
/** Provider-neutral host preference mapped to the Apps in Toss banner appearance. */
|
|
151
|
+
readonly bannerAppearance?: AitBannerAppearance;
|
|
137
152
|
readonly dependencies?: Partial<AitHostDependencies>;
|
|
138
153
|
}
|
|
139
154
|
export declare function installAitHostBridge(options?: InstallAitHostBridgeOptions): GamePlatformBridge;
|
package/dist/host.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { getTossShareLink, getUserKeyForGame, grantPromotionRewardForGame as grantPromotionReward, IAP, isMinVersionSupported, loadFullScreenAd, openGameCenterLeaderboard, requestNotificationAgreement, share, showFullScreenAd, Storage, submitGameCenterLeaderBoardScore, } from '@apps-in-toss/web-framework';
|
|
1
|
+
import { getTossShareLink, getUserKeyForGame, grantPromotionRewardForGame as grantPromotionReward, IAP, isMinVersionSupported, loadFullScreenAd, openGameCenterLeaderboard, requestNotificationAgreement, share, showFullScreenAd, Storage, submitGameCenterLeaderBoardScore, TossAds, } 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;
|
|
5
5
|
const defaultAdLoadQueueTimeoutMs = 5000;
|
|
6
6
|
const defaultAdDisplayStartTimeoutMs = 60000;
|
|
7
7
|
const defaultAdMaximumDisplayMs = 30 * 60000;
|
|
8
|
+
const defaultBannerRenderTimeoutMs = 15000;
|
|
8
9
|
/** Leaves five seconds of headroom inside the native IAP 30-second grant callback. */
|
|
9
10
|
const defaultIapProductGrantTimeoutMs = 25000;
|
|
10
11
|
/** Bounds abandoned checkout UI while still allowing the user to finish an interactive purchase. */
|
|
@@ -53,6 +54,7 @@ const defaultDependencies = {
|
|
|
53
54
|
isMinVersionSupported,
|
|
54
55
|
loadFullScreenAd,
|
|
55
56
|
showFullScreenAd,
|
|
57
|
+
tossAds: TossAds,
|
|
56
58
|
openGameCenterLeaderboard,
|
|
57
59
|
submitGameCenterLeaderBoardScore,
|
|
58
60
|
iap: IAP,
|
|
@@ -80,11 +82,21 @@ export function createAitHostBridge(options = {}) {
|
|
|
80
82
|
const loadedAdGroupIds = new Set();
|
|
81
83
|
const loadingAdGroups = new Map();
|
|
82
84
|
const activeAdGroupIds = new Set();
|
|
85
|
+
const activeBannerAttachments = new Map();
|
|
86
|
+
const bannerMountOwners = new Map();
|
|
87
|
+
const pendingBannerMountSettlements = new Map();
|
|
88
|
+
let bannerInitialization;
|
|
83
89
|
let warnedUnsupportedAdPreload = false;
|
|
84
90
|
const adTimeoutMs = normalizeTimeout(options.adTimeoutMs);
|
|
85
91
|
const adLoadQueueTimeoutMs = normalizeLoadQueueTimeout(options.adLoadQueueTimeoutMs);
|
|
86
92
|
const adDisplayStartTimeoutMs = normalizeDisplayStartTimeout(options.adDisplayStartTimeoutMs);
|
|
87
93
|
const adMaximumDisplayMs = normalizeMaximumDisplayTimeout(options.adMaximumDisplayMs);
|
|
94
|
+
const bannerRenderTimeoutMs = normalizeBannerRenderTimeout(options.bannerRenderTimeoutMs);
|
|
95
|
+
const bannerAppearance = {
|
|
96
|
+
theme: options.bannerAppearance?.theme ?? 'dark',
|
|
97
|
+
tone: options.bannerAppearance?.tone ?? 'blackAndWhite',
|
|
98
|
+
variant: options.bannerAppearance?.variant ?? 'expanded',
|
|
99
|
+
};
|
|
88
100
|
const adLoadCoordinator = {
|
|
89
101
|
active: undefined,
|
|
90
102
|
waitTimeoutMs: adLoadQueueTimeoutMs,
|
|
@@ -116,6 +128,8 @@ export function createAitHostBridge(options = {}) {
|
|
|
116
128
|
&& hasConfiguredAdType(adGroupIds, adPlacementTypes, 'rewarded');
|
|
117
129
|
const interstitialAds = adsSupported
|
|
118
130
|
&& hasConfiguredAdType(adGroupIds, adPlacementTypes, 'interstitial');
|
|
131
|
+
const bannerAds = areBannerAdsSupported(dependencies)
|
|
132
|
+
&& hasConfiguredAdType(adGroupIds, adPlacementTypes, 'banner');
|
|
119
133
|
const nativeIap = isAitIapSupported({
|
|
120
134
|
dependencies,
|
|
121
135
|
products: iapProducts,
|
|
@@ -126,7 +140,8 @@ export function createAitHostBridge(options = {}) {
|
|
|
126
140
|
});
|
|
127
141
|
return ok(request, {
|
|
128
142
|
nativeIap,
|
|
129
|
-
nativeAds: rewardedAds || interstitialAds,
|
|
143
|
+
nativeAds: rewardedAds || interstitialAds || bannerAds,
|
|
144
|
+
bannerAds,
|
|
130
145
|
rewardedAds,
|
|
131
146
|
interstitialAds,
|
|
132
147
|
nativeLeaderboard: isGameCenterSupported(dependencies),
|
|
@@ -315,6 +330,9 @@ export function createAitHostBridge(options = {}) {
|
|
|
315
330
|
if (adGroupId === undefined || placementType === undefined) {
|
|
316
331
|
return createBridgeError(request.id, 'AIT_AD_UNAVAILABLE', `AIT ad placement is unavailable: ${placementId}`);
|
|
317
332
|
}
|
|
333
|
+
if (placementType === 'banner') {
|
|
334
|
+
return ok(request, {});
|
|
335
|
+
}
|
|
318
336
|
if (!isAitNativeMethodSupported(dependencies.loadFullScreenAd)) {
|
|
319
337
|
if (!warnedUnsupportedAdPreload) {
|
|
320
338
|
warnedUnsupportedAdPreload = true;
|
|
@@ -370,6 +388,61 @@ export function createAitHostBridge(options = {}) {
|
|
|
370
388
|
const shown = await withLoadedAdSlot(dependencies, adGroupId, loadedAdGroupIds, loadingAdGroups, adLoadCoordinator, activeAdGroupIds, adTimeoutMs, 'interstitial', showInterstitial);
|
|
371
389
|
return ok(request, shown.acquired ? shown.value : { status: 'unavailable' });
|
|
372
390
|
}
|
|
391
|
+
case 'ads.mountBanner': {
|
|
392
|
+
const placementId = readPlacementId(request.payload);
|
|
393
|
+
const surfaceId = readBannerSurfaceId(request.payload);
|
|
394
|
+
const adGroupId = adGroupIds.get(placementId);
|
|
395
|
+
if (adGroupId === undefined
|
|
396
|
+
|| adPlacementTypes.get(placementId) !== 'banner'
|
|
397
|
+
|| !areBannerAdsSupported(dependencies)) {
|
|
398
|
+
return ok(request, { status: 'unavailable' });
|
|
399
|
+
}
|
|
400
|
+
const target = globalThis.document?.getElementById(surfaceId);
|
|
401
|
+
if (target === null || target === undefined) {
|
|
402
|
+
return ok(request, { status: 'failed' });
|
|
403
|
+
}
|
|
404
|
+
pendingBannerMountSettlements.get(surfaceId)?.settleUnavailable();
|
|
405
|
+
const mountOwner = Symbol(surfaceId);
|
|
406
|
+
bannerMountOwners.set(surfaceId, mountOwner);
|
|
407
|
+
activeBannerAttachments.get(surfaceId)?.destroy();
|
|
408
|
+
activeBannerAttachments.delete(surfaceId);
|
|
409
|
+
const initializeBanner = bannerInitialization
|
|
410
|
+
?? initializeAitBannerAds(dependencies, bannerRenderTimeoutMs);
|
|
411
|
+
void (bannerInitialization = initializeBanner);
|
|
412
|
+
if (!await initializeBanner) {
|
|
413
|
+
bannerInitialization = undefined;
|
|
414
|
+
if (bannerMountOwners.get(surfaceId) === mountOwner) {
|
|
415
|
+
bannerMountOwners.delete(surfaceId);
|
|
416
|
+
}
|
|
417
|
+
return ok(request, { status: 'failed' });
|
|
418
|
+
}
|
|
419
|
+
if (bannerMountOwners.get(surfaceId) !== mountOwner) {
|
|
420
|
+
return ok(request, { status: 'unavailable' });
|
|
421
|
+
}
|
|
422
|
+
activeBannerAttachments.get(surfaceId)?.destroy();
|
|
423
|
+
activeBannerAttachments.delete(surfaceId);
|
|
424
|
+
const result = await attachAitBanner({
|
|
425
|
+
dependencies,
|
|
426
|
+
adGroupId,
|
|
427
|
+
surfaceId,
|
|
428
|
+
target,
|
|
429
|
+
timeoutMs: bannerRenderTimeoutMs,
|
|
430
|
+
appearance: bannerAppearance,
|
|
431
|
+
activeAttachments: activeBannerAttachments,
|
|
432
|
+
mountOwner,
|
|
433
|
+
mountOwners: bannerMountOwners,
|
|
434
|
+
pendingSettlements: pendingBannerMountSettlements,
|
|
435
|
+
});
|
|
436
|
+
return ok(request, result);
|
|
437
|
+
}
|
|
438
|
+
case 'ads.unmountBanner': {
|
|
439
|
+
const surfaceId = readBannerSurfaceId(request.payload);
|
|
440
|
+
pendingBannerMountSettlements.get(surfaceId)?.settleUnavailable();
|
|
441
|
+
bannerMountOwners.delete(surfaceId);
|
|
442
|
+
activeBannerAttachments.get(surfaceId)?.destroy();
|
|
443
|
+
activeBannerAttachments.delete(surfaceId);
|
|
444
|
+
return ok(request, {});
|
|
445
|
+
}
|
|
373
446
|
case 'leaderboard.submitScore': {
|
|
374
447
|
if (!isGameCenterSupported(dependencies)) {
|
|
375
448
|
return ok(request, { submitted: false });
|
|
@@ -1837,6 +1910,122 @@ function areFullScreenAdsSupported(dependencies) {
|
|
|
1837
1910
|
return isAitNativeMethodSupported(dependencies.loadFullScreenAd)
|
|
1838
1911
|
&& isAitNativeMethodSupported(dependencies.showFullScreenAd);
|
|
1839
1912
|
}
|
|
1913
|
+
function areBannerAdsSupported(dependencies) {
|
|
1914
|
+
return isAitNativeMethodSupported(dependencies.tossAds.initialize)
|
|
1915
|
+
&& isAitNativeMethodSupported(dependencies.tossAds.attachBanner);
|
|
1916
|
+
}
|
|
1917
|
+
function initializeAitBannerAds(dependencies, timeoutMs) {
|
|
1918
|
+
return new Promise((resolve) => {
|
|
1919
|
+
let settled = false;
|
|
1920
|
+
const finish = (initialized) => {
|
|
1921
|
+
if (settled) {
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
settled = true;
|
|
1925
|
+
globalThis.clearTimeout(timer);
|
|
1926
|
+
resolve(initialized);
|
|
1927
|
+
};
|
|
1928
|
+
const timer = globalThis.setTimeout(() => finish(false), timeoutMs);
|
|
1929
|
+
try {
|
|
1930
|
+
dependencies.tossAds.initialize({
|
|
1931
|
+
callbacks: {
|
|
1932
|
+
onInitialized: () => finish(true),
|
|
1933
|
+
onInitializationFailed: () => finish(false),
|
|
1934
|
+
},
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
catch (error) {
|
|
1938
|
+
console.warn('Failed to initialize AIT banner ads.', error);
|
|
1939
|
+
finish(false);
|
|
1940
|
+
}
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
async function attachAitBanner(input) {
|
|
1944
|
+
return new Promise((resolve) => {
|
|
1945
|
+
let settled = false;
|
|
1946
|
+
let terminalStatus;
|
|
1947
|
+
let attachment;
|
|
1948
|
+
const finish = (requestedStatus) => {
|
|
1949
|
+
const ownsSurface = input.mountOwners.get(input.surfaceId) === input.mountOwner;
|
|
1950
|
+
const status = requestedStatus === 'mounted' && !ownsSurface
|
|
1951
|
+
? 'unavailable'
|
|
1952
|
+
: requestedStatus;
|
|
1953
|
+
if (settled) {
|
|
1954
|
+
if (status !== 'mounted' && input.activeAttachments.get(input.surfaceId) === attachment) {
|
|
1955
|
+
attachment?.destroy();
|
|
1956
|
+
input.activeAttachments.delete(input.surfaceId);
|
|
1957
|
+
}
|
|
1958
|
+
if (status !== 'mounted' && ownsSurface) {
|
|
1959
|
+
input.mountOwners.delete(input.surfaceId);
|
|
1960
|
+
}
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
settled = true;
|
|
1964
|
+
terminalStatus = status;
|
|
1965
|
+
globalThis.clearTimeout(timer);
|
|
1966
|
+
if (input.pendingSettlements.get(input.surfaceId)?.mountOwner === input.mountOwner) {
|
|
1967
|
+
input.pendingSettlements.delete(input.surfaceId);
|
|
1968
|
+
}
|
|
1969
|
+
if (status !== 'mounted') {
|
|
1970
|
+
attachment?.destroy();
|
|
1971
|
+
if (input.activeAttachments.get(input.surfaceId) === attachment) {
|
|
1972
|
+
input.activeAttachments.delete(input.surfaceId);
|
|
1973
|
+
}
|
|
1974
|
+
if (ownsSurface) {
|
|
1975
|
+
input.mountOwners.delete(input.surfaceId);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
resolve({ status });
|
|
1979
|
+
};
|
|
1980
|
+
const timer = globalThis.setTimeout(() => finish('failed'), input.timeoutMs);
|
|
1981
|
+
input.pendingSettlements.set(input.surfaceId, {
|
|
1982
|
+
mountOwner: input.mountOwner,
|
|
1983
|
+
settleUnavailable: () => finish('unavailable'),
|
|
1984
|
+
});
|
|
1985
|
+
try {
|
|
1986
|
+
const sdkAttachment = input.dependencies.tossAds.attachBanner(input.adGroupId, input.target, {
|
|
1987
|
+
theme: input.appearance.theme,
|
|
1988
|
+
tone: input.appearance.tone,
|
|
1989
|
+
variant: input.appearance.variant,
|
|
1990
|
+
callbacks: {
|
|
1991
|
+
onAdRendered: () => finish('mounted'),
|
|
1992
|
+
onNoFill: () => finish('unavailable'),
|
|
1993
|
+
onAdFailedToRender: (payload) => {
|
|
1994
|
+
console.warn('Failed to render an AIT banner ad.', input.adGroupId, payload.error);
|
|
1995
|
+
finish('failed');
|
|
1996
|
+
},
|
|
1997
|
+
},
|
|
1998
|
+
});
|
|
1999
|
+
let attachmentDestroyed = false;
|
|
2000
|
+
attachment = {
|
|
2001
|
+
destroy() {
|
|
2002
|
+
if (!attachmentDestroyed) {
|
|
2003
|
+
attachmentDestroyed = true;
|
|
2004
|
+
sdkAttachment.destroy();
|
|
2005
|
+
}
|
|
2006
|
+
},
|
|
2007
|
+
};
|
|
2008
|
+
if (terminalStatus === 'unavailable' || terminalStatus === 'failed') {
|
|
2009
|
+
attachment.destroy();
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
if (input.mountOwners.get(input.surfaceId) !== input.mountOwner) {
|
|
2013
|
+
attachment.destroy();
|
|
2014
|
+
finish('unavailable');
|
|
2015
|
+
return;
|
|
2016
|
+
}
|
|
2017
|
+
const existingAttachment = input.activeAttachments.get(input.surfaceId);
|
|
2018
|
+
if (existingAttachment !== undefined && existingAttachment !== attachment) {
|
|
2019
|
+
existingAttachment.destroy();
|
|
2020
|
+
}
|
|
2021
|
+
input.activeAttachments.set(input.surfaceId, attachment);
|
|
2022
|
+
}
|
|
2023
|
+
catch (error) {
|
|
2024
|
+
console.warn('Failed to attach an AIT banner ad.', input.adGroupId, error);
|
|
2025
|
+
finish('failed');
|
|
2026
|
+
}
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
1840
2029
|
function isAitNativeMethodSupported(method) {
|
|
1841
2030
|
return isCapabilitySupported(() => method.isSupported?.() === true);
|
|
1842
2031
|
}
|
|
@@ -1930,6 +2119,9 @@ function normalizeDisplayStartTimeout(value) {
|
|
|
1930
2119
|
function normalizeMaximumDisplayTimeout(value) {
|
|
1931
2120
|
return normalizePositiveTimeout(value, defaultAdMaximumDisplayMs, 'AIT ad maximum display timeout must be a positive finite number.');
|
|
1932
2121
|
}
|
|
2122
|
+
function normalizeBannerRenderTimeout(value) {
|
|
2123
|
+
return normalizePositiveTimeout(value, defaultBannerRenderTimeoutMs, 'AIT banner render timeout must be a positive finite number.');
|
|
2124
|
+
}
|
|
1933
2125
|
function normalizeIapProductGrantTimeout(value) {
|
|
1934
2126
|
const timeout = normalizePositiveTimeout(value, defaultIapProductGrantTimeoutMs, 'AIT IAP product-grant timeout must be a positive finite number.');
|
|
1935
2127
|
if (timeout > defaultIapProductGrantTimeoutMs) {
|
|
@@ -1960,6 +2152,13 @@ function readPlacementId(payload) {
|
|
|
1960
2152
|
}
|
|
1961
2153
|
return placementId;
|
|
1962
2154
|
}
|
|
2155
|
+
function readBannerSurfaceId(payload) {
|
|
2156
|
+
const surfaceId = readPayloadRecord(payload).surfaceId;
|
|
2157
|
+
if (typeof surfaceId !== 'string' || surfaceId.length === 0) {
|
|
2158
|
+
throw new TypeError('AIT banner surfaceId must be a valid non-empty element id.');
|
|
2159
|
+
}
|
|
2160
|
+
return surfaceId;
|
|
2161
|
+
}
|
|
1963
2162
|
function readIdempotencyKey(payload, fallback) {
|
|
1964
2163
|
const value = readPayloadRecord(payload).idempotencyKey;
|
|
1965
2164
|
return typeof value === 'string' && value.length > 0 ? value : fallback;
|
package/dist/index.js
CHANGED
|
@@ -58,6 +58,8 @@ export function createAitPlatformGateway(input) {
|
|
|
58
58
|
preload: (payload) => request('ads.preload', payload),
|
|
59
59
|
showRewarded: (payload) => request('ads.showRewarded', payload),
|
|
60
60
|
showInterstitial: (payload) => request('ads.showInterstitial', payload),
|
|
61
|
+
mountBanner: (payload) => request('ads.mountBanner', payload),
|
|
62
|
+
unmountBanner: (payload) => request('ads.unmountBanner', payload),
|
|
61
63
|
},
|
|
62
64
|
leaderboard: {
|
|
63
65
|
submitScore: (payload) => request('leaderboard.submitScore', payload),
|
|
@@ -95,6 +97,7 @@ export function createAitSandboxBridge(options = {}) {
|
|
|
95
97
|
return ok(input, {
|
|
96
98
|
nativeIap: true,
|
|
97
99
|
nativeAds: true,
|
|
100
|
+
bannerAds: false,
|
|
98
101
|
rewardedAds: true,
|
|
99
102
|
interstitialAds: true,
|
|
100
103
|
nativeLeaderboard: true,
|
|
@@ -178,6 +181,10 @@ export function createAitSandboxBridge(options = {}) {
|
|
|
178
181
|
return ok(input, {
|
|
179
182
|
status: 'shown',
|
|
180
183
|
});
|
|
184
|
+
case 'ads.mountBanner':
|
|
185
|
+
return ok(input, { status: 'unavailable' });
|
|
186
|
+
case 'ads.unmountBanner':
|
|
187
|
+
return ok(input, {});
|
|
181
188
|
case 'leaderboard.submitScore':
|
|
182
189
|
return ok(input, {
|
|
183
190
|
submitted: true,
|
package/dist/local-mock.d.ts
CHANGED
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
* commerce: those paths must be verified in the Toss app sandbox/console.
|
|
7
7
|
*/
|
|
8
8
|
declare function unsupported(): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Local browsers have no native safe-area constant. Throwing preserves the
|
|
11
|
+
* wrapper's CSS `env(safe-area-inset-*)` fallback while keeping ESM exports in
|
|
12
|
+
* parity with the SDK surface consumed by the adapter.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SafeArea: {
|
|
15
|
+
get(): never;
|
|
16
|
+
subscribe(_input: {
|
|
17
|
+
readonly onEvent: (insets: unknown) => void;
|
|
18
|
+
}): () => void;
|
|
19
|
+
};
|
|
9
20
|
export declare const Storage: {
|
|
10
21
|
getItem(key: string): Promise<string | null>;
|
|
11
22
|
setItem(key: string, value: string): Promise<void>;
|
|
@@ -30,6 +41,22 @@ export declare const loadFullScreenAd: ((_callbacks: unknown) => (() => void)) &
|
|
|
30
41
|
export declare const showFullScreenAd: ((_callbacks: unknown) => (() => void)) & {
|
|
31
42
|
isSupported: typeof unsupported;
|
|
32
43
|
};
|
|
44
|
+
export declare const TossAds: {
|
|
45
|
+
initialize: ((_options: unknown) => void) & {
|
|
46
|
+
isSupported: typeof unsupported;
|
|
47
|
+
};
|
|
48
|
+
attachBanner: ((_adGroupId: string, _target: string | HTMLElement, _options?: unknown) => {
|
|
49
|
+
destroy(): void;
|
|
50
|
+
}) & {
|
|
51
|
+
isSupported: typeof unsupported;
|
|
52
|
+
};
|
|
53
|
+
destroy: ((_slotId: string) => void) & {
|
|
54
|
+
isSupported: typeof unsupported;
|
|
55
|
+
};
|
|
56
|
+
destroyAll: (() => void) & {
|
|
57
|
+
isSupported: typeof unsupported;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
33
60
|
export declare function openGameCenterLeaderboard(): Promise<void>;
|
|
34
61
|
export declare function submitGameCenterLeaderBoardScore(): Promise<{
|
|
35
62
|
readonly statusCode: 'ERROR';
|
package/dist/local-mock.js
CHANGED
|
@@ -12,6 +12,19 @@ function unsupported() {
|
|
|
12
12
|
function noOpSubscription() {
|
|
13
13
|
return () => { };
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Local browsers have no native safe-area constant. Throwing preserves the
|
|
17
|
+
* wrapper's CSS `env(safe-area-inset-*)` fallback while keeping ESM exports in
|
|
18
|
+
* parity with the SDK surface consumed by the adapter.
|
|
19
|
+
*/
|
|
20
|
+
export const SafeArea = {
|
|
21
|
+
get() {
|
|
22
|
+
throw new Error('SafeArea is unavailable in the Apps in Toss local mock.');
|
|
23
|
+
},
|
|
24
|
+
subscribe(_input) {
|
|
25
|
+
return noOpSubscription();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
15
28
|
export const Storage = {
|
|
16
29
|
async getItem(key) {
|
|
17
30
|
return localStorageValues.get(key) ?? null;
|
|
@@ -39,6 +52,14 @@ export const grantPromotionRewardForGame = Object.assign(async () => 'ERROR', {
|
|
|
39
52
|
export const requestNotificationAgreement = Object.assign((_callbacks) => noOpSubscription(), { isSupported: unsupported });
|
|
40
53
|
export const loadFullScreenAd = Object.assign((_callbacks) => noOpSubscription(), { isSupported: unsupported });
|
|
41
54
|
export const showFullScreenAd = Object.assign((_callbacks) => noOpSubscription(), { isSupported: unsupported });
|
|
55
|
+
export const TossAds = {
|
|
56
|
+
initialize: Object.assign((_options) => { }, { isSupported: unsupported }),
|
|
57
|
+
attachBanner: Object.assign((_adGroupId, _target, _options) => ({
|
|
58
|
+
destroy() { },
|
|
59
|
+
}), { isSupported: unsupported }),
|
|
60
|
+
destroy: Object.assign((_slotId) => { }, { isSupported: unsupported }),
|
|
61
|
+
destroyAll: Object.assign(() => { }, { isSupported: unsupported }),
|
|
62
|
+
};
|
|
42
63
|
export async function openGameCenterLeaderboard() { }
|
|
43
64
|
export async function submitGameCenterLeaderBoardScore() {
|
|
44
65
|
return { statusCode: 'ERROR' };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Raw Apps in Toss safe-area values expressed in CSS pixels. */
|
|
2
|
+
export interface AitSafeAreaInsets {
|
|
3
|
+
readonly top: number;
|
|
4
|
+
readonly right: number;
|
|
5
|
+
readonly bottom: number;
|
|
6
|
+
readonly left: number;
|
|
7
|
+
}
|
|
8
|
+
/** Minimal SDK surface kept injectable for deterministic wrapper tests. */
|
|
9
|
+
export interface AitSafeAreaSource {
|
|
10
|
+
get(): unknown;
|
|
11
|
+
subscribe(input: {
|
|
12
|
+
readonly onEvent: (insets: unknown) => void;
|
|
13
|
+
}): () => void;
|
|
14
|
+
}
|
|
15
|
+
/** Minimal CSS declaration surface required by the bridge. */
|
|
16
|
+
export interface AitSafeAreaCssStyle {
|
|
17
|
+
getPropertyValue(property: string): string;
|
|
18
|
+
getPropertyPriority?(property: string): string;
|
|
19
|
+
setProperty(property: string, value: string, priority?: string): void;
|
|
20
|
+
removeProperty(property: string): string;
|
|
21
|
+
}
|
|
22
|
+
export interface InstallAitSafeAreaCssVariablesOptions {
|
|
23
|
+
readonly source?: AitSafeAreaSource;
|
|
24
|
+
readonly style?: AitSafeAreaCssStyle;
|
|
25
|
+
/** Override only when a host version or custom navigation treatment has a measured band. */
|
|
26
|
+
readonly navigationControlBandHeightPx?: number;
|
|
27
|
+
/** Override only when native navigation uses a measured right-edge gap. */
|
|
28
|
+
readonly navigationControlEdgeGapPx?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Apps in Toss positions the game navigation controls immediately below the
|
|
32
|
+
* native top safe area. Keep full-width app chrome below this additional band.
|
|
33
|
+
*/
|
|
34
|
+
export declare const aitNavigationControlBandHeightPx = 52;
|
|
35
|
+
/** Apps in Toss navigation controls retain this edge gap from the safe right inset. */
|
|
36
|
+
export declare const aitNavigationControlEdgeGapPx = 10;
|
|
37
|
+
/** CSS contract shared by generated AIT wrappers and full-bleed games. */
|
|
38
|
+
export declare const aitSafeAreaCssVariables: Readonly<{
|
|
39
|
+
readonly top: '--mpgd-safe-area-top';
|
|
40
|
+
readonly right: '--mpgd-safe-area-right';
|
|
41
|
+
readonly bottom: '--mpgd-safe-area-bottom';
|
|
42
|
+
readonly left: '--mpgd-safe-area-left';
|
|
43
|
+
readonly navigationControlTop: '--mpgd-ait-navigation-control-top';
|
|
44
|
+
readonly navigationControlRight: '--mpgd-ait-navigation-control-right';
|
|
45
|
+
readonly navigationContentTop: '--mpgd-ait-navigation-content-top';
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Mirror the official AIT `SafeArea` snapshot into CSS custom properties and
|
|
49
|
+
* keep them current when the WebView changes presentation or orientation.
|
|
50
|
+
*
|
|
51
|
+
* CSS `env(safe-area-inset-*)` remains the wrapper's pre-SDK fallback. Invalid
|
|
52
|
+
* or unavailable native values never overwrite that fallback.
|
|
53
|
+
*/
|
|
54
|
+
export declare function installAitSafeAreaCssVariables(options?: InstallAitSafeAreaCssVariablesOptions): () => void;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { SafeArea } from '@apps-in-toss/web-framework';
|
|
2
|
+
/**
|
|
3
|
+
* Apps in Toss positions the game navigation controls immediately below the
|
|
4
|
+
* native top safe area. Keep full-width app chrome below this additional band.
|
|
5
|
+
*/
|
|
6
|
+
export const aitNavigationControlBandHeightPx = 52;
|
|
7
|
+
/** Apps in Toss navigation controls retain this edge gap from the safe right inset. */
|
|
8
|
+
export const aitNavigationControlEdgeGapPx = 10;
|
|
9
|
+
/** CSS contract shared by generated AIT wrappers and full-bleed games. */
|
|
10
|
+
export const aitSafeAreaCssVariables = Object.freeze({
|
|
11
|
+
top: '--mpgd-safe-area-top',
|
|
12
|
+
right: '--mpgd-safe-area-right',
|
|
13
|
+
bottom: '--mpgd-safe-area-bottom',
|
|
14
|
+
left: '--mpgd-safe-area-left',
|
|
15
|
+
navigationControlTop: '--mpgd-ait-navigation-control-top',
|
|
16
|
+
navigationControlRight: '--mpgd-ait-navigation-control-right',
|
|
17
|
+
navigationContentTop: '--mpgd-ait-navigation-content-top',
|
|
18
|
+
});
|
|
19
|
+
const maximumSafeAreaInsetPx = 4096;
|
|
20
|
+
const maximumCssPixelOptionPx = 4096;
|
|
21
|
+
/**
|
|
22
|
+
* Mirror the official AIT `SafeArea` snapshot into CSS custom properties and
|
|
23
|
+
* keep them current when the WebView changes presentation or orientation.
|
|
24
|
+
*
|
|
25
|
+
* CSS `env(safe-area-inset-*)` remains the wrapper's pre-SDK fallback. Invalid
|
|
26
|
+
* or unavailable native values never overwrite that fallback.
|
|
27
|
+
*/
|
|
28
|
+
export function installAitSafeAreaCssVariables(options = {}) {
|
|
29
|
+
const navigationControlBandHeightPx = normalizeCssPixelOption(options.navigationControlBandHeightPx, aitNavigationControlBandHeightPx, 'navigationControlBandHeightPx');
|
|
30
|
+
const navigationControlEdgeGapPx = normalizeCssPixelOption(options.navigationControlEdgeGapPx, aitNavigationControlEdgeGapPx, 'navigationControlEdgeGapPx');
|
|
31
|
+
const style = options.style ?? resolveDocumentRootStyle();
|
|
32
|
+
if (style === undefined) {
|
|
33
|
+
return () => { };
|
|
34
|
+
}
|
|
35
|
+
const source = options.source ?? SafeArea;
|
|
36
|
+
const previous = rememberCssVariables(style);
|
|
37
|
+
let active = true;
|
|
38
|
+
let unsubscribe = () => { };
|
|
39
|
+
const apply = (input) => {
|
|
40
|
+
if (!active) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const insets = normalizeAitSafeAreaInsets(input);
|
|
44
|
+
if (insets === null) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
style.setProperty(aitSafeAreaCssVariables.top, formatCssPixels(insets.top));
|
|
48
|
+
style.setProperty(aitSafeAreaCssVariables.right, formatCssPixels(insets.right));
|
|
49
|
+
style.setProperty(aitSafeAreaCssVariables.bottom, formatCssPixels(insets.bottom));
|
|
50
|
+
style.setProperty(aitSafeAreaCssVariables.left, formatCssPixels(insets.left));
|
|
51
|
+
style.setProperty(aitSafeAreaCssVariables.navigationControlTop, formatCssPixels(insets.top));
|
|
52
|
+
style.setProperty(aitSafeAreaCssVariables.navigationControlRight, formatCssPixels(insets.right + navigationControlEdgeGapPx));
|
|
53
|
+
style.setProperty(aitSafeAreaCssVariables.navigationContentTop, formatCssPixels(insets.top + navigationControlBandHeightPx));
|
|
54
|
+
};
|
|
55
|
+
try {
|
|
56
|
+
apply(source.get());
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Local browsers and older Toss hosts keep the CSS env() fallback.
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const subscribed = source.subscribe({ onEvent: apply });
|
|
63
|
+
unsubscribe = typeof subscribed === 'function' ? subscribed : () => { };
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// A missing native event bridge must not prevent the game from booting.
|
|
67
|
+
}
|
|
68
|
+
return () => {
|
|
69
|
+
if (!active) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
active = false;
|
|
73
|
+
try {
|
|
74
|
+
unsubscribe();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
restoreCssVariables(style, previous);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function normalizeAitSafeAreaInsets(input) {
|
|
82
|
+
if (typeof input !== 'object' || input === null) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const record = input;
|
|
86
|
+
const top = normalizeInset(record.top);
|
|
87
|
+
const right = normalizeInset(record.right);
|
|
88
|
+
const bottom = normalizeInset(record.bottom);
|
|
89
|
+
const left = normalizeInset(record.left);
|
|
90
|
+
if (top === null || right === null || bottom === null || left === null) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return { top, right, bottom, left };
|
|
94
|
+
}
|
|
95
|
+
function normalizeInset(input) {
|
|
96
|
+
return typeof input === 'number'
|
|
97
|
+
&& Number.isFinite(input)
|
|
98
|
+
&& input >= 0
|
|
99
|
+
&& input <= maximumSafeAreaInsetPx
|
|
100
|
+
? input
|
|
101
|
+
: null;
|
|
102
|
+
}
|
|
103
|
+
function normalizeCssPixelOption(input, fallback, name) {
|
|
104
|
+
const value = input ?? fallback;
|
|
105
|
+
if (!Number.isFinite(value) || value < 0 || value > maximumCssPixelOptionPx) {
|
|
106
|
+
throw new TypeError(`${name} must be a non-negative finite CSS pixel value.`);
|
|
107
|
+
}
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function formatCssPixels(value) {
|
|
111
|
+
return `${Object.is(value, -0) ? 0 : value}px`;
|
|
112
|
+
}
|
|
113
|
+
function resolveDocumentRootStyle() {
|
|
114
|
+
return typeof document === 'undefined' ? undefined : document.documentElement.style;
|
|
115
|
+
}
|
|
116
|
+
function rememberCssVariables(style) {
|
|
117
|
+
return Object.values(aitSafeAreaCssVariables).map((property) => ({
|
|
118
|
+
property,
|
|
119
|
+
value: style.getPropertyValue(property),
|
|
120
|
+
priority: style.getPropertyPriority?.(property) ?? '',
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
function restoreCssVariables(style, previous) {
|
|
124
|
+
for (const entry of previous) {
|
|
125
|
+
if (entry.value.length === 0) {
|
|
126
|
+
style.removeProperty(entry.property);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
style.setProperty(entry.property, entry.value, entry.priority);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mpgd/adapter-ait",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Apps in Toss WebView platform adapter for mpgd games.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -45,14 +45,18 @@
|
|
|
45
45
|
"types": "./dist/local-mock.d.ts",
|
|
46
46
|
"default": "./dist/local-mock.js"
|
|
47
47
|
},
|
|
48
|
+
"./safe-area": {
|
|
49
|
+
"types": "./dist/safe-area.d.ts",
|
|
50
|
+
"default": "./dist/safe-area.js"
|
|
51
|
+
},
|
|
48
52
|
"./package.json": "./package.json"
|
|
49
53
|
},
|
|
50
54
|
"files": [
|
|
51
55
|
"dist"
|
|
52
56
|
],
|
|
53
57
|
"dependencies": {
|
|
54
|
-
"@mpgd/bridge": "0.
|
|
55
|
-
"@mpgd/platform": "0.
|
|
58
|
+
"@mpgd/bridge": "0.9.0",
|
|
59
|
+
"@mpgd/platform": "0.10.0"
|
|
56
60
|
},
|
|
57
61
|
"peerDependencies": {
|
|
58
62
|
"@apps-in-toss/web-framework": ">=3.0.0 <4"
|
|
@@ -63,7 +67,7 @@
|
|
|
63
67
|
}
|
|
64
68
|
},
|
|
65
69
|
"devDependencies": {
|
|
66
|
-
"@apps-in-toss/web-framework": "3.
|
|
70
|
+
"@apps-in-toss/web-framework": "3.1.1",
|
|
67
71
|
"ttsc": "0.18.4",
|
|
68
72
|
"typescript": "7.0.2",
|
|
69
73
|
"vitest": "^4.0.0"
|