@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.
@@ -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, 'rewarded' | 'interstitial'>>;
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 !== 'rewarded' && placement.type !== 'interstitial')) {
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, 'rewarded' | 'interstitial'>>;
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,
@@ -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';
@@ -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.9.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.8.0",
55
- "@mpgd/platform": "0.9.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.0.2",
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"