@mpgd/adapter-capacitor 0.6.2 → 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 +29 -5
- package/dist/admob.d.ts +4 -0
- package/dist/admob.js +243 -94
- package/dist/index.js +3 -0
- package/dist/providers.d.ts +3 -0
- package/package.json +8 -7
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
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
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
|
231
|
+
return noStart('transient-failure');
|
|
131
232
|
}
|
|
132
|
-
const customData = encodeAdMobSsvCustomData({
|
|
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
|
|
239
|
+
return noStart('transient-failure');
|
|
135
240
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
rewardEarned = true;
|
|
268
|
+
void preparation.then(() => {
|
|
269
|
+
if (loadTimedOut) {
|
|
270
|
+
pendingLoadSdk.delete(sdk);
|
|
147
271
|
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
})
|
|
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
|
-
|
|
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
|
|
174
|
-
return
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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(
|
|
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),
|
package/dist/providers.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
47
|
-
"@mpgd/capacitor-game-services": "0.6.
|
|
48
|
-
"@mpgd/game-services": "0.
|
|
49
|
-
"@mpgd/platform": "0.
|
|
50
|
-
"@mpgd/target-config": "0.17.
|
|
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",
|