@mpgd/adapter-capacitor 0.6.1 → 0.7.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/README.md CHANGED
@@ -122,10 +122,15 @@ an unverified local guest label. Re-check consent when the game resumes or
122
122
  the privacy choice changes, and expose `admob.showPrivacyOptions()` from game
123
123
  settings where required. `getCapabilities()` reports `action-required` until
124
124
  consent is ready and `temporarily-unavailable` during a show. A timed-out
125
- native load or presentation reports non-retryable `action-required` and
126
- quarantines that SDK instance to avoid overlapping shows
127
- while its state is unknown. Recreating the Kit provider with the same SDK does
128
- not clear the quarantine; restart the native app before trying again. Do not
125
+ native load or presentation reports `action-required` and quarantines that
126
+ SDK instance to avoid overlapping shows. A timed-out load fails before display
127
+ and remains quarantined until that original preparation settles; it never
128
+ opens an ad later. A display timeout returns pending even if an SDK reward was
129
+ observed, because earning does not prove closure. The provider retains native
130
+ terminal listeners and clears presentation uncertainty after a confirmed late
131
+ close. Listener registration/cleanup failures remain quarantined until native
132
+ app restart. Recreating a Kit provider with the same SDK does not clear these
133
+ guards. Do not
129
134
  show rewarded ads through another direct SDK caller concurrently: the plugin's
130
135
  reward and dismissal events are global, not tagged with a Kit operation ID.
131
136
 
@@ -142,6 +147,20 @@ for a later signed SSV callback without replaying the ad UI; a genuinely
142
147
  skipped ad may remain pending until game-owned support or expiry policy
143
148
  settles it. Mediation and asynchronous bridge delivery have no safe universal
144
149
  250 ms reward-event cutoff.
150
+
151
+ The gateway exposes `ads.provider` with protocol `mpgd.ads.v2`. Its rewarded
152
+ preparation is `deferred`, since the operation's SSV binding must be attached to
153
+ the fresh load. Missing placements are `configuration-required`, consent is
154
+ `action-required`, and interstitials are explicitly unsupported by this provider.
155
+ Native `Showed`, `Dismissed`, and `FailedToShow` events establish presentation
156
+ facts; the original `showRewardVideoAd` Promise establishes per-call SDK
157
+ eligibility. Global `Rewarded` events carry no invocation identity and cannot
158
+ bind a late reward to a new show. A requested event carries
159
+ `claimEvidence` for server proof lookup under the original idempotency key;
160
+ this does not assert local eligibility or a grant. Pass this actual provider to
161
+ the shared [game runtime](../../packages/game-runtime/README.md#native-presentation-and-financial-settlement)
162
+ to coordinate execution and deadlines above all scenes. Removing SDK listeners
163
+ and disposing a view cannot establish native closure.
145
164
  The receiver and ledger described in [AdMob SSV](../../docs/ADMOB_SSV.md)
146
165
  remain mandatory. A late SSV callback can leave the claim pending for
147
166
  reconciliation. `isTesting: true` is for SDK test ads only; [the plugin notes
@@ -149,7 +168,12 @@ that test ads do not invoke the SSV endpoint](https://github.com/capacitor-commu
149
168
  so they cannot prove a production grant.
150
169
 
151
170
  The Kit tests cover consent, per-operation binding, reward versus dismissal,
152
- and bridge contracts with an injected SDK. They are not evidence of a real
171
+ and bridge contracts with an injected SDK. Seven shared presentation vectors
172
+ run through this real provider, the installed Capacitor gateway, and the common
173
+ runtime: consent/action, configuration, policy, deferred preparation, deadline
174
+ with late closure, full-screen arbitration, and background ownership. This is
175
+ presentation coverage, not complete certification of the server-ledger vector
176
+ matrix. The fixtures are not evidence of a real
153
177
  AdMob account, live callback delivery, native device behavior, or store policy
154
178
  approval.
155
179
 
package/dist/admob.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type AdMobPlugin } from '@capacitor-community/admob';
2
+ import { type AdProvider } from '@mpgd/platform/ads';
2
3
  import type { CapacitorServiceProvider } from './providers.js';
3
4
  /** Only the AdMob subpath imports the optional native SDK. */
4
5
  export type RewardedAdMobSdk = Pick<AdMobPlugin, 'initialize' | 'requestConsentInfo' | 'showConsentForm' | 'showPrivacyOptionsForm' | 'prepareRewardVideoAd' | 'showRewardVideoAd' | 'addListener'>;
@@ -11,8 +12,11 @@ export interface CreateCapacitorAdMobRewardedProviderInput {
11
12
  readonly isTesting?: boolean;
12
13
  readonly sdk?: RewardedAdMobSdk;
13
14
  readonly showTimeoutMs?: number;
15
+ /** Used for native no-fill error codes; defaults to Capacitor's platform. */
16
+ readonly target?: 'android' | 'ios';
14
17
  }
15
18
  export interface CapacitorAdMobRewardedProvider extends CapacitorServiceProvider {
19
+ readonly adProvider: AdProvider;
16
20
  /** Present UMP when required. Call before the gateway advertises ad readiness. */
17
21
  requestConsent(): Promise<boolean>;
18
22
  /** Game settings should expose this when the privacy message requires it. */
package/dist/admob.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { AdMob, AdmobConsentStatus, RewardAdPluginEvents, } from '@capacitor-community/admob';
2
+ import { Capacitor } from '@capacitor/core';
2
3
  import { admobClientRewardEvidenceSchema } from '@mpgd/game-services/admob-client-reward';
3
4
  import { admobSsvMaximumBindingFieldLength, encodeAdMobSsvCustomData, } from '@mpgd/game-services/admob-ssv';
5
+ import { adProtocol, adProtocolVersion, assertAdPresentationEvent, assertAdShowInput, assertAdShowResult, toAdAdapter, } from '@mpgd/platform/ads';
4
6
  const unitPattern = /^ca-app-pub-\d+\/\d+$/u;
5
7
  const defaultShowTimeoutMs = 180_000;
6
8
  const loadTimeoutMs = 30_000;
@@ -11,6 +13,7 @@ const maximumCustomDataBytes = 1_024;
11
13
  // Coordinate Kit providers sharing one SDK instance, even across gateways.
12
14
  const activeSdk = new WeakSet();
13
15
  const uncertainSdk = new WeakSet();
16
+ const pendingLoadSdk = new WeakSet();
14
17
  function response(input, result) {
15
18
  return { id: input.id, ok: true, data: result };
16
19
  }
@@ -106,116 +109,245 @@ export function createCapacitorAdMobRewardedProvider(input) {
106
109
  }
107
110
  return consentTask;
108
111
  };
109
- const showRewarded = async (placementId, idempotencyKey) => {
110
- const adId = adUnits.get(placementId);
111
- if (adId === undefined || !canRequestAds || activeSdk.has(sdk) || uncertainSdk.has(sdk)) {
112
- return { status: 'unavailable', rewardGranted: false };
112
+ const providerId = 'admob-rewarded';
113
+ const nativeTarget = input.target ?? Capacitor.getPlatform();
114
+ const listeners = new Set();
115
+ const availability = (request) => {
116
+ if (request.format !== 'rewarded') {
117
+ return { state: 'unsupported', reason: 'unsupported' };
113
118
  }
114
- if (idempotencyKey.trim() === ''
115
- || idempotencyKey.length > admobSsvMaximumBindingFieldLength
116
- || placementId.length > admobSsvMaximumBindingFieldLength) {
117
- return { status: 'failed', rewardGranted: false };
119
+ if (!adUnits.has(request.placementId)) {
120
+ return {
121
+ state: 'configuration-required',
122
+ reason: 'configuration-required',
123
+ };
124
+ }
125
+ if (!canRequestAds || uncertainSdk.has(sdk) || pendingLoadSdk.has(sdk)) {
126
+ return {
127
+ state: 'action-required',
128
+ reason: 'action-required',
129
+ };
130
+ }
131
+ if (activeSdk.has(sdk)) {
132
+ return { state: 'temporarily-unavailable', reason: 'busy' };
133
+ }
134
+ return { state: 'available' };
135
+ };
136
+ const show = async (supplied) => {
137
+ const request = assertAdShowInput(supplied);
138
+ const available = availability(request);
139
+ const result = (state) => assertAdShowResult({
140
+ providerId,
141
+ invocationId: request.invocationId,
142
+ format: request.format,
143
+ ...state,
144
+ });
145
+ const noStart = (reason, outcome = 'failed') => result({
146
+ outcome,
147
+ presentation: 'not-started',
148
+ eligibility: request.format === 'rewarded' ? 'not-earned' : 'not-applicable',
149
+ reason,
150
+ });
151
+ if (available.state !== 'available') {
152
+ return noStart(available.reason ?? 'unsupported', 'unavailable');
153
+ }
154
+ if (request.idempotencyKey.length > admobSsvMaximumBindingFieldLength
155
+ || request.placementId.length > admobSsvMaximumBindingFieldLength) {
156
+ return noStart('transient-failure');
157
+ }
158
+ const adId = adUnits.get(request.placementId);
159
+ if (adId === undefined) {
160
+ return noStart('configuration-required', 'unavailable');
118
161
  }
119
162
  activeSdk.add(sdk);
120
- const handles = [];
163
+ let sequence = 0;
164
+ let started = false;
165
+ let displayCalled = false;
166
+ let closed = false;
121
167
  let rewardEarned = false;
168
+ let loadTimedOut = false;
169
+ let listenerTimedOut = false;
170
+ let loadFailure = 'transient-failure';
122
171
  let timer;
123
172
  let loadTimer;
124
- let loadTimedOut = false;
125
- let nativePreflightStarted = false;
173
+ const handles = [];
174
+ const candidate = { schema: admobClientRewardEvidenceSchema, payload: { adUnitId: adId } };
175
+ const emit = (event) => {
176
+ const published = assertAdPresentationEvent({
177
+ ...event,
178
+ providerId,
179
+ invocationId: request.invocationId,
180
+ sequence: ++sequence,
181
+ });
182
+ for (const listener of [...listeners]) {
183
+ try {
184
+ void Promise.resolve(listener(published)).catch(() => undefined);
185
+ }
186
+ catch { /* Native observation must continue. */ }
187
+ }
188
+ };
189
+ let cleanupTask;
190
+ const cleanup = () => {
191
+ cleanupTask ??= (async () => {
192
+ const settled = await Promise.allSettled(handles.map((handle) => withTimeout(Promise.resolve().then(() => handle.remove()), listenerCleanupTimeoutMs)));
193
+ if (settled.some((entry) => entry.status === 'rejected')) {
194
+ uncertainSdk.add(sdk);
195
+ }
196
+ activeSdk.delete(sdk);
197
+ })();
198
+ return cleanupTask;
199
+ };
200
+ const earned = (reward) => {
201
+ // Global Rewarded events lack invocation identity. Only the original
202
+ // show promise can bind a late earned callback to this operation.
203
+ if (!displayCalled || rewardEarned || !isRewardItem(reward)) {
204
+ return;
205
+ }
206
+ rewardEarned = true;
207
+ emit({ type: 'reward-earned', evidence: candidate });
208
+ };
209
+ let finish;
210
+ const terminal = new Promise((resolve) => {
211
+ finish = resolve;
212
+ });
213
+ const close = (failed) => {
214
+ if (!displayCalled || closed) {
215
+ return;
216
+ }
217
+ closed = true;
218
+ uncertainSdk.delete(sdk);
219
+ if (timer !== undefined) {
220
+ clearTimeout(timer);
221
+ }
222
+ emit(failed ? { type: 'failed', reason: 'transient-failure' } : { type: 'closed' });
223
+ finish(failed ? 'failed' : 'dismissed');
224
+ // Also runs after the caller-facing promise has already returned pending.
225
+ void cleanup();
226
+ };
126
227
  try {
127
228
  const playerId = await withTimeout(Promise.resolve().then(() => input.getPlayerId()), preflightTimeoutMs);
128
229
  if (typeof playerId !== 'string' || playerId.trim() === ''
129
230
  || playerId.length > admobSsvMaximumBindingFieldLength) {
130
- return { status: 'failed', rewardGranted: false };
231
+ return noStart('transient-failure');
131
232
  }
132
- const customData = encodeAdMobSsvCustomData({ playerId, placementId, idempotencyKey });
233
+ const customData = encodeAdMobSsvCustomData({
234
+ playerId,
235
+ placementId: request.placementId,
236
+ idempotencyKey: request.idempotencyKey,
237
+ });
133
238
  if (new TextEncoder().encode(customData).byteLength > maximumCustomDataBytes) {
134
- return { status: 'failed', rewardGranted: false };
239
+ return noStart('transient-failure');
135
240
  }
136
- // This plugin resolves showRewardVideoAd on reward, but on Android a
137
- // dismissal without reward leaves that promise unresolved. Observe the
138
- // terminal native events separately, and hold lifecycle through close.
139
- let finish;
140
- const terminal = new Promise((resolve) => {
141
- finish = resolve;
241
+ const listen = async (promise) => {
242
+ try {
243
+ handles.push(await withTimeout(promise, preflightTimeoutMs, (handle) => handle.remove()));
244
+ }
245
+ catch (error) {
246
+ listenerTimedOut = error instanceof PreflightTimeoutError;
247
+ throw error;
248
+ }
249
+ };
250
+ await listen(sdk.addListener(RewardAdPluginEvents.Dismissed, () => queueMicrotask(() => close(false))));
251
+ await listen(sdk.addListener(RewardAdPluginEvents.FailedToShow, () => close(true)));
252
+ await listen(sdk.addListener(RewardAdPluginEvents.Showed, () => {
253
+ if (displayCalled && !closed && !started) {
254
+ started = true;
255
+ emit({ type: 'started' });
256
+ }
257
+ }));
258
+ await listen(sdk.addListener(RewardAdPluginEvents.FailedToLoad, (error) => {
259
+ if (nativeTarget === 'android' && (error.code === 3 || error.code === 9) || nativeTarget === 'ios' && error.code === 1) {
260
+ loadFailure = 'no-fill';
261
+ }
262
+ }));
263
+ const preparation = sdk.prepareRewardVideoAd({
264
+ adId,
265
+ ...(input.isTesting === undefined ? {} : { isTesting: input.isTesting }),
266
+ ssv: { userId: playerId, customData },
142
267
  });
143
- nativePreflightStarted = true;
144
- handles.push(await withTimeout(sdk.addListener(RewardAdPluginEvents.Rewarded, (reward) => {
145
- if (isRewardItem(reward)) {
146
- rewardEarned = true;
268
+ void preparation.then(() => {
269
+ if (loadTimedOut) {
270
+ pendingLoadSdk.delete(sdk);
147
271
  }
148
- }), preflightTimeoutMs, (handle) => handle.remove()));
149
- handles.push(await withTimeout(sdk.addListener(RewardAdPluginEvents.Dismissed, () => {
150
- // Let a same-turn Rewarded event settle before reading the flag.
151
- queueMicrotask(() => finish('dismissed'));
152
- }), preflightTimeoutMs, (handle) => handle.remove()));
153
- handles.push(await withTimeout(sdk.addListener(RewardAdPluginEvents.FailedToShow, () => {
154
- finish('failed');
155
- }), preflightTimeoutMs, (handle) => handle.remove()));
272
+ }, () => {
273
+ if (loadTimedOut) {
274
+ pendingLoadSdk.delete(sdk);
275
+ }
276
+ });
156
277
  const loaded = await Promise.race([
157
- sdk.prepareRewardVideoAd({
158
- adId,
159
- ...(input.isTesting === undefined ? {} : { isTesting: input.isTesting }),
160
- ssv: { userId: playerId, customData },
161
- }),
278
+ preparation,
162
279
  new Promise((_resolve, reject) => {
163
- loadTimer = setTimeout(() => {
164
- loadTimedOut = true;
165
- reject(new Error('AdMob load timed out.'));
166
- }, loadTimeoutMs);
280
+ loadTimer = setTimeout(() => { loadTimedOut = true; pendingLoadSdk.add(sdk); reject(new Error('AdMob load timed out.')); }, loadTimeoutMs);
167
281
  }),
168
282
  ]);
169
283
  if (loadTimer !== undefined) {
170
284
  clearTimeout(loadTimer);
171
285
  loadTimer = undefined;
172
286
  }
173
- if (typeof loaded.adUnitId !== 'string' || loaded.adUnitId.length === 0) {
174
- return { status: 'failed', rewardGranted: false };
175
- }
176
- timer = setTimeout(() => finish('timeout'), showTimeoutMs);
177
- // Always observe rejection, even if a Dismissed event wins the race.
178
- const showResult = sdk.showRewardVideoAd({ adId: loaded.adUnitId });
179
- const onReward = (reward) => {
180
- if (isRewardItem(reward)) {
181
- rewardEarned = true;
182
- }
183
- };
184
- const onShowError = () => finish('failed');
185
- void showResult.then(onReward, onShowError);
186
- const outcome = await terminal;
187
- if (outcome === 'timeout') {
188
- // Native presentation state is unknown: do not allow another show.
189
- uncertainSdk.add(sdk);
287
+ if (typeof loaded.adUnitId !== 'string' || !unitPattern.test(loaded.adUnitId)) {
288
+ return noStart('transient-failure');
190
289
  }
191
- let status = 'failed';
192
- if (rewardEarned) {
193
- status = 'completed';
290
+ // At this point SSV is attached to the original operation. Lookup correlation
291
+ // is useful even when the caller's deadline precedes an earned SDK callback.
292
+ emit({ type: 'requested', claimEvidence: candidate });
293
+ displayCalled = true;
294
+ timer = setTimeout(() => { uncertainSdk.add(sdk); emit({ type: 'unknown' }); finish('timeout'); }, showTimeoutMs);
295
+ let showing;
296
+ try {
297
+ showing = sdk.showRewardVideoAd({ adId: loaded.adUnitId });
194
298
  }
195
- else if (outcome === 'dismissed') {
196
- // Mediation may deliver reward after dismissal. Only signed SSV can
197
- // later distinguish a true skip from an earned reward.
198
- status = 'pending';
299
+ catch {
300
+ showing = Promise.reject(new Error('AdMob show invocation failed.'));
199
301
  }
200
- else if (outcome === 'timeout') {
201
- status = 'pending';
302
+ void showing.then((reward) => earned(reward), () => {
303
+ // Promise rejection alone is not a native close observation.
304
+ if (!closed) {
305
+ uncertainSdk.add(sdk);
306
+ emit({ type: 'unknown' });
307
+ finish('timeout');
308
+ }
309
+ });
310
+ const outcome = await terminal;
311
+ if (outcome === 'failed') {
312
+ if (!started && !rewardEarned) {
313
+ return noStart('transient-failure');
314
+ }
315
+ return result({
316
+ outcome: 'failed',
317
+ presentation: 'closed',
318
+ eligibility: rewardEarned ? 'eligible' : 'unknown',
319
+ reason: 'transient-failure',
320
+ claimEvidence: candidate,
321
+ ...(rewardEarned ? { evidence: candidate } : {}),
322
+ });
202
323
  }
203
- return {
204
- status,
205
- rewardGranted: false,
206
- ...(status === 'completed' || status === 'pending' ? { evidence: {
207
- schema: admobClientRewardEvidenceSchema,
208
- payload: { adUnitId: adId },
209
- } } : {}),
210
- };
324
+ const eligibility = rewardEarned ? 'eligible' : 'unknown';
325
+ return result({
326
+ outcome: outcome === 'timeout' ? 'pending' : 'shown',
327
+ presentation: outcome === 'timeout' ? 'unknown' : 'closed',
328
+ eligibility,
329
+ ...(outcome === 'timeout' ? { reason: 'outcome-unknown' } : {}),
330
+ claimEvidence: candidate,
331
+ ...(rewardEarned ? { evidence: candidate } : {}),
332
+ });
211
333
  }
212
- catch (error) {
213
- const uncertain = loadTimedOut
214
- || (nativePreflightStarted && error instanceof PreflightTimeoutError);
215
- if (uncertain) {
334
+ catch {
335
+ if (displayCalled && !closed) {
336
+ uncertainSdk.add(sdk);
337
+ emit({ type: 'unknown' });
338
+ return result({
339
+ outcome: 'pending',
340
+ presentation: 'unknown',
341
+ eligibility: rewardEarned ? 'eligible' : 'unknown',
342
+ reason: 'outcome-unknown',
343
+ claimEvidence: candidate,
344
+ ...(rewardEarned ? { evidence: candidate } : {}),
345
+ });
346
+ }
347
+ if (listenerTimedOut) {
216
348
  uncertainSdk.add(sdk);
217
349
  }
218
- return { status: uncertain ? 'pending' : 'failed', rewardGranted: false };
350
+ return noStart(loadFailure);
219
351
  }
220
352
  finally {
221
353
  if (loadTimer !== undefined) {
@@ -224,27 +356,41 @@ export function createCapacitorAdMobRewardedProvider(input) {
224
356
  if (timer !== undefined) {
225
357
  clearTimeout(timer);
226
358
  }
227
- try {
228
- const removals = handles.map((handle) => {
229
- const removal = Promise.resolve().then(() => handle.remove());
230
- return withTimeout(removal, listenerCleanupTimeoutMs);
231
- });
232
- const settled = await Promise.allSettled(removals);
233
- if (settled.some((result) => result.status === 'rejected')) {
234
- uncertainSdk.add(sdk);
235
- }
236
- }
237
- finally {
238
- activeSdk.delete(sdk);
359
+ // A caller deadline cannot remove the only native close observer. Keep
360
+ // listeners/ownership until a definitive close, and release only this SDK.
361
+ if (!displayCalled || closed) {
362
+ await cleanup();
239
363
  }
240
364
  }
241
365
  };
366
+ const adProvider = {
367
+ id: providerId,
368
+ protocol: adProtocol,
369
+ protocolVersion: adProtocolVersion,
370
+ rewardSignal: 'delayed',
371
+ getAvailability: async (request) => availability(request),
372
+ async preload(request) {
373
+ const available = availability(request);
374
+ return available.state === 'available'
375
+ ? { status: 'deferred' }
376
+ : { status: 'unavailable', reason: available.reason ?? 'unsupported' };
377
+ },
378
+ show,
379
+ subscribe(listener) {
380
+ listeners.add(listener);
381
+ return () => {
382
+ listeners.delete(listener);
383
+ };
384
+ },
385
+ };
386
+ const legacy = toAdAdapter(adProvider);
242
387
  return {
243
388
  id: 'admob-rewarded',
389
+ adProvider,
244
390
  features: ['rewardedAds'],
245
391
  methods: ['ads.preload', 'ads.showRewarded'],
246
392
  async getAvailability() {
247
- if (uncertainSdk.has(sdk) || !canRequestAds) {
393
+ if (uncertainSdk.has(sdk) || pendingLoadSdk.has(sdk) || !canRequestAds) {
248
394
  return { rewardedAds: 'action-required' };
249
395
  }
250
396
  return { rewardedAds: activeSdk.has(sdk) ? 'temporarily-unavailable' : 'available' };
@@ -281,7 +427,10 @@ export function createCapacitorAdMobRewardedProvider(input) {
281
427
  || typeof request.payload.idempotencyKey !== 'string') {
282
428
  return failure(request, 'ADMOB_REQUEST_INVALID');
283
429
  }
284
- return response(request, await showRewarded(request.payload.placementId, request.payload.idempotencyKey));
430
+ return response(request, await legacy.showRewarded({
431
+ placementId: request.payload.placementId,
432
+ idempotencyKey: request.payload.idempotencyKey,
433
+ }));
285
434
  },
286
435
  },
287
436
  };
package/dist/index.js CHANGED
@@ -44,6 +44,8 @@ function assertCredentialMutationResponse(input, field) {
44
44
  export function createCapacitorPlatformGateway(input) {
45
45
  const bridge = input.bridge ?? CapacitorGameServices;
46
46
  const providers = createCapacitorProviderRegistry(input.providers ?? []);
47
+ const adProvider = providers.byMethod.get('ads.showRewarded')?.adProvider
48
+ ?? providers.byMethod.get('ads.showInterstitial')?.adProvider;
47
49
  const lifecycle = createCapacitorAppEvents({
48
50
  target: input.target,
49
51
  ...(input.app === undefined ? {} : { app: input.app }),
@@ -199,6 +201,7 @@ export function createCapacitorPlatformGateway(input) {
199
201
  getEntitlements: () => request('commerce.getEntitlements', {}),
200
202
  },
201
203
  ads: {
204
+ ...(adProvider === undefined ? {} : { provider: adProvider }),
202
205
  preload: (payload) => request('ads.preload', payload),
203
206
  showRewarded: (payload) => request('ads.showRewarded', payload),
204
207
  showInterstitial: (payload) => request('ads.showInterstitial', payload),
@@ -1,4 +1,5 @@
1
1
  import type { BridgeMethod, BridgeRequest, BridgeResponse } from '@mpgd/bridge';
2
+ import type { AdProvider } from '@mpgd/platform/ads';
2
3
  import { type PlatformProviderAvailability, type PlatformProviderFeature, type ProductType } from '@mpgd/platform';
3
4
  export interface NativeBridge {
4
5
  request(input: BridgeRequest): Promise<BridgeResponse>;
@@ -14,6 +15,8 @@ export interface CapacitorServiceProvider {
14
15
  readonly bridge: NativeBridge;
15
16
  readonly features: readonly PlatformProviderFeature[];
16
17
  readonly methods: readonly BridgeMethod[];
18
+ /** Optional native event surface. The base adapter never imports its SDK. */
19
+ readonly adProvider?: AdProvider;
17
20
  getAvailability(): Promise<Readonly<Partial<Record<PlatformProviderFeature, PlatformProviderAvailability>>>>;
18
21
  }
19
22
  export interface CapacitorProviderRegistry {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mpgd/adapter-capacitor",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Capacitor platform adapter for mpgd Android and iOS shells.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,11 +43,11 @@
43
43
  ],
44
44
  "dependencies": {
45
45
  "@capacitor/app": "8.1.1",
46
- "@mpgd/bridge": "0.10.0",
47
- "@mpgd/platform": "0.14.0",
48
- "@mpgd/target-config": "0.17.0",
49
- "@mpgd/capacitor-game-services": "0.6.0",
50
- "@mpgd/game-services": "0.17.1"
46
+ "@mpgd/bridge": "0.11.0",
47
+ "@mpgd/capacitor-game-services": "0.6.1",
48
+ "@mpgd/game-services": "0.18.0",
49
+ "@mpgd/platform": "0.15.0",
50
+ "@mpgd/target-config": "0.17.1"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@apple/app-store-server-library": "^3.1.0",
@@ -68,7 +68,8 @@
68
68
  "@capacitor/core": "8.5.2",
69
69
  "ttsc": "0.30.4",
70
70
  "typescript": "7.0.2",
71
- "vitest": "^4.0.0"
71
+ "vitest": "^4.0.0",
72
+ "@mpgd/game-runtime": "0.3.0"
72
73
  },
73
74
  "main": "./dist/index.js",
74
75
  "types": "./dist/index.d.ts",