@chipmobilesdk/rn-ads 0.5.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/CHANGELOG.md +178 -0
- package/README.md +12 -3
- package/package.json +5 -2
- package/skills/sdk-rn-ads/SKILL.md +3 -2
- package/src/config/validate.ts +3 -1
- package/src/core/adsInstance.ts +85 -1872
- package/src/core/consentFlow.ts +1 -20
- package/src/core/instance/consent.ts +162 -0
- package/src/core/instance/context.ts +328 -0
- package/src/core/instance/gate.ts +147 -0
- package/src/core/instance/init.ts +262 -0
- package/src/core/instance/internals.ts +138 -0
- package/src/core/instance/load.ts +426 -0
- package/src/core/instance/show.ts +202 -0
- package/src/core/instance/types.ts +198 -0
- package/src/core/requestThrottle.ts +11 -17
- package/src/core/retryPolicy.ts +12 -2
- package/src/core/timers.ts +17 -69
- package/src/diagnostics/errors.ts +52 -63
- package/src/diagnostics/events.ts +77 -66
- package/src/engine/admobBanner.tsx +55 -0
- package/src/engine/admobEngine.ts +38 -18
- package/src/engine/admobErrors.ts +44 -0
- package/src/engine/port.ts +46 -1
- package/src/engine/testAdUnits.ts +0 -5
- package/src/index.ts +19 -1
- package/src/react/AdsBanner.tsx +94 -79
- package/src/react/AdsProvider.tsx +6 -7
- package/src/react/hooks.ts +26 -31
- package/src/testing/clock.ts +75 -0
- package/src/testing/conformance.ts +112 -0
- package/src/testing/index.ts +43 -70
|
@@ -19,7 +19,14 @@ import type {
|
|
|
19
19
|
UnavailableReason,
|
|
20
20
|
Unsubscribe,
|
|
21
21
|
} from '../config/types';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
createDiagnosticChannel,
|
|
24
|
+
createEmitter,
|
|
25
|
+
type DiagnosticSeverity,
|
|
26
|
+
type Emitter,
|
|
27
|
+
} from '@chipmobilesdk/rn-core';
|
|
28
|
+
|
|
29
|
+
import { AdsError, type AdsOperation } from './errors';
|
|
23
30
|
|
|
24
31
|
export type AdEventType =
|
|
25
32
|
| 'initialization'
|
|
@@ -151,79 +158,83 @@ export interface AdEventEmitter {
|
|
|
151
158
|
listenerCount(): number;
|
|
152
159
|
}
|
|
153
160
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
type: 'internal_error',
|
|
175
|
-
timestamp: Date.now(),
|
|
176
|
-
placementKey: event.placementKey,
|
|
177
|
-
format: event.format,
|
|
178
|
-
correlationId: event.correlationId,
|
|
179
|
-
error: new AdsError('internal_error' as AdsErrorCode, {
|
|
180
|
-
message: 'consumer event listener threw',
|
|
181
|
-
cause: error,
|
|
182
|
-
}),
|
|
183
|
-
});
|
|
184
|
-
} finally {
|
|
185
|
-
reportingListenerFailure = false;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
}
|
|
161
|
+
/**
|
|
162
|
+
* How many once-per-cycle keys the emitter remembers. A cycle's duplicates arrive
|
|
163
|
+
* within that cycle, so forgetting the oldest keys past this bound loses nothing,
|
|
164
|
+
* and memory stays flat however long the instance lives (CORE-12, ADS-05).
|
|
165
|
+
*/
|
|
166
|
+
const DEDUPE_CAPACITY = 256;
|
|
167
|
+
|
|
168
|
+
/** Every delivered event also goes to the app's SDK sinks (`installSdkSink`, CORE-08). */
|
|
169
|
+
const channel = createDiagnosticChannel<AdEventType>('rn-ads');
|
|
170
|
+
|
|
171
|
+
function severityOf(event: AdEvent): DiagnosticSeverity {
|
|
172
|
+
switch (event.type) {
|
|
173
|
+
case 'internal_error':
|
|
174
|
+
return 'error';
|
|
175
|
+
case 'load_failed':
|
|
176
|
+
case 'display_failed':
|
|
177
|
+
case 'config_warning':
|
|
178
|
+
return 'warn';
|
|
179
|
+
default:
|
|
180
|
+
return 'info';
|
|
189
181
|
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** What a sink may see: where and which cycle, and an error's code. Never a payload. */
|
|
185
|
+
function contextOf(event: AdEvent): Record<string, unknown> {
|
|
186
|
+
return {
|
|
187
|
+
...(event.placementKey === undefined ? {} : { placementKey: event.placementKey }),
|
|
188
|
+
...(event.format === undefined ? {} : { format: event.format }),
|
|
189
|
+
...('error' in event ? { code: event.error.code, retryable: event.error.retryable } : {}),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function createEventEmitter(): AdEventEmitter {
|
|
194
|
+
// The event whose listeners are running, so a listener's failure is reported
|
|
195
|
+
// against the placement and cycle it broke (FR-056).
|
|
196
|
+
let dispatching: AdEvent | null = null;
|
|
197
|
+
const emitter: Emitter<AdEvent> = createEmitter<AdEvent>({
|
|
198
|
+
onListenerError(error) {
|
|
199
|
+
emitter.emit({
|
|
200
|
+
type: 'internal_error',
|
|
201
|
+
timestamp: Date.now(),
|
|
202
|
+
placementKey: dispatching?.placementKey,
|
|
203
|
+
format: dispatching?.format,
|
|
204
|
+
correlationId: dispatching?.correlationId,
|
|
205
|
+
error: new AdsError('internal_error', {
|
|
206
|
+
message: 'consumer event listener threw',
|
|
207
|
+
cause: error,
|
|
208
|
+
}),
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
dedupe: {
|
|
212
|
+
key: event =>
|
|
213
|
+
event.correlationId && ONCE_PER_CYCLE.has(event.type)
|
|
214
|
+
? `${event.correlationId}:${event.type}`
|
|
215
|
+
: undefined,
|
|
216
|
+
capacity: DEDUPE_CAPACITY,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
190
219
|
|
|
191
220
|
return {
|
|
192
221
|
emit(event) {
|
|
193
222
|
// After dispose, a late provider callback updates nothing and emits
|
|
194
|
-
// nothing (FR-035, SC-008).
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (emittedOnce.has(key)) {
|
|
201
|
-
return;
|
|
223
|
+
// nothing (FR-035, SC-008); the rn-core emitter enforces both.
|
|
224
|
+
const previous = dispatching;
|
|
225
|
+
dispatching = event;
|
|
226
|
+
try {
|
|
227
|
+
if (emitter.emit(event)) {
|
|
228
|
+
channel.emit(event.type, severityOf(event), contextOf(event), event.correlationId);
|
|
202
229
|
}
|
|
203
|
-
|
|
230
|
+
} finally {
|
|
231
|
+
dispatching = previous;
|
|
204
232
|
}
|
|
205
|
-
dispatch(event);
|
|
206
|
-
},
|
|
207
|
-
subscribe(listener) {
|
|
208
|
-
listeners.add(listener);
|
|
209
|
-
let active = true;
|
|
210
|
-
return () => {
|
|
211
|
-
// Idempotent: a second call is a no-op, not an error (FR-005).
|
|
212
|
-
if (!active) {
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
active = false;
|
|
216
|
-
listeners.delete(listener);
|
|
217
|
-
};
|
|
218
|
-
},
|
|
219
|
-
dispose() {
|
|
220
|
-
disposed = true;
|
|
221
|
-
listeners.clear();
|
|
222
|
-
emittedOnce.clear();
|
|
223
|
-
},
|
|
224
|
-
listenerCount() {
|
|
225
|
-
return listeners.size;
|
|
226
233
|
},
|
|
234
|
+
// Idempotent unsubscribe (FR-005).
|
|
235
|
+
subscribe: listener => emitter.subscribe(listener),
|
|
236
|
+
dispose: () => emitter.dispose(),
|
|
237
|
+
listenerCount: () => emitter.listenerCount(),
|
|
227
238
|
};
|
|
228
239
|
}
|
|
229
240
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The AdMob adaptive banner behind the engine port (ADS-12).
|
|
3
|
+
*
|
|
4
|
+
* `AdsBanner` renders whatever banner component the engine offers and passes
|
|
5
|
+
* neutral props; this file is where they become AdMob's. So the banner host no
|
|
6
|
+
* longer requires the bridge itself, a fake engine can render a banner in a test
|
|
7
|
+
* without mocking a native module, and an iOS or other-provider engine supplies
|
|
8
|
+
* its own component.
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import type { ComponentType } from 'react';
|
|
12
|
+
|
|
13
|
+
import { classifyProviderFailure } from './admobErrors';
|
|
14
|
+
import type { EngineBanner, EngineBannerProps } from './port';
|
|
15
|
+
|
|
16
|
+
/** The slice of the bridge the banner uses. */
|
|
17
|
+
interface ProviderBannerModule {
|
|
18
|
+
readonly BannerAd?: ComponentType<Record<string, unknown>>;
|
|
19
|
+
readonly BannerAdSize?: Record<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createAdMobBanner(getProvider: () => ProviderBannerModule | null): EngineBanner {
|
|
23
|
+
function AdMobBanner({
|
|
24
|
+
adUnitId,
|
|
25
|
+
personalizedAds,
|
|
26
|
+
onLoaded,
|
|
27
|
+
onFailed,
|
|
28
|
+
onImpression,
|
|
29
|
+
onClicked,
|
|
30
|
+
}: EngineBannerProps) {
|
|
31
|
+
const provider = getProvider();
|
|
32
|
+
const BannerAd = provider?.BannerAd;
|
|
33
|
+
if (!BannerAd) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return (
|
|
37
|
+
<BannerAd
|
|
38
|
+
unitId={adUnitId}
|
|
39
|
+
// Adaptive, so the banner fills the container the app measured (FR-038).
|
|
40
|
+
size={provider.BannerAdSize?.ANCHORED_ADAPTIVE_BANNER}
|
|
41
|
+
// The provider's polarity is the inverse of the consumer-facing option.
|
|
42
|
+
requestOptions={{ requestNonPersonalizedAdsOnly: !personalizedAds }}
|
|
43
|
+
onAdLoaded={() => onLoaded()}
|
|
44
|
+
onAdFailedToLoad={(error: unknown) => onFailed(error, classifyProviderFailure(error))}
|
|
45
|
+
onAdImpression={() => onImpression()}
|
|
46
|
+
onAdClicked={() => onClicked()}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
isAvailable: () => Boolean(getProvider()?.BannerAd),
|
|
53
|
+
Component: AdMobBanner,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -16,9 +16,15 @@
|
|
|
16
16
|
* applied.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { lazyPeer } from '@chipmobilesdk/rn-core';
|
|
20
|
+
|
|
21
|
+
import type { ComponentType } from 'react';
|
|
19
22
|
import { Platform } from 'react-native';
|
|
20
23
|
|
|
21
24
|
import type { AdContentRating, FullScreenFormat, Unsubscribe } from '../config/types';
|
|
25
|
+
import { normalizeReward } from '../diagnostics/events';
|
|
26
|
+
import { createAdMobBanner } from './admobBanner';
|
|
27
|
+
import { classifyProviderFailure } from './admobErrors';
|
|
22
28
|
import type {
|
|
23
29
|
AdsEngine,
|
|
24
30
|
ConsentPort,
|
|
@@ -56,6 +62,8 @@ interface ProviderModule {
|
|
|
56
62
|
MaxAdContentRating: Record<string, string>;
|
|
57
63
|
AdsConsent: ProviderConsent;
|
|
58
64
|
AdsConsentDebugGeography: Record<string, number>;
|
|
65
|
+
BannerAd?: ComponentType<Record<string, unknown>>;
|
|
66
|
+
BannerAdSize?: Record<string, string>;
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
/** The slice of the bridge's consent module this adapter uses. */
|
|
@@ -141,15 +149,24 @@ function toEnginePrivacyOptions(value: string | undefined): EngineConsentInfo['p
|
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
|
|
144
|
-
|
|
152
|
+
/**
|
|
153
|
+
* The provider bridge, loaded on first use (rn-core's lazy peer). It is a required
|
|
154
|
+
* peer, but a consumer can still have it missing mid-install or excluded from a
|
|
155
|
+
* build: reporting `provider_missing` beats crashing the host app's launch
|
|
156
|
+
* (FR-025, NFR-005). The banner component reaches it through the same loader.
|
|
157
|
+
*/
|
|
158
|
+
export const admobProvider = lazyPeer('react-native-google-mobile-ads', () => {
|
|
159
|
+
// Inside `try`, so Metro bundles an app that leaves this optional peer out.
|
|
145
160
|
try {
|
|
146
161
|
return require('react-native-google-mobile-ads') as ProviderModule;
|
|
147
|
-
} catch {
|
|
148
|
-
|
|
149
|
-
// mid-install or excluded from a build. Reporting `provider_missing`
|
|
150
|
-
// beats crashing the host app's launch (FR-025, NFR-005).
|
|
151
|
-
return null;
|
|
162
|
+
} catch (cause) {
|
|
163
|
+
throw cause;
|
|
152
164
|
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
function loadProvider(): ProviderModule | null {
|
|
168
|
+
const result = admobProvider.get();
|
|
169
|
+
return result.ok ? result.module : null;
|
|
153
170
|
}
|
|
154
171
|
|
|
155
172
|
function toProviderRating(
|
|
@@ -207,6 +224,10 @@ export function createAdMobEngine(): AdsEngine {
|
|
|
207
224
|
return supported && getProvider() !== null;
|
|
208
225
|
},
|
|
209
226
|
|
|
227
|
+
// The adaptive banner renders through the engine like every other format
|
|
228
|
+
// (ADS-12), so the banner host never names the provider.
|
|
229
|
+
banner: createAdMobBanner(getProvider),
|
|
230
|
+
|
|
210
231
|
async initialize(privacy: EnginePrivacy) {
|
|
211
232
|
const module = getProvider();
|
|
212
233
|
if (!module) {
|
|
@@ -297,7 +318,11 @@ export function createAdMobEngine(): AdsEngine {
|
|
|
297
318
|
};
|
|
298
319
|
|
|
299
320
|
on(AdEventType.LOADED, () => ({ type: 'loaded' }));
|
|
300
|
-
on(AdEventType.ERROR, cause => ({
|
|
321
|
+
on(AdEventType.ERROR, cause => ({
|
|
322
|
+
type: 'failed',
|
|
323
|
+
cause,
|
|
324
|
+
kind: classifyProviderFailure(cause),
|
|
325
|
+
}));
|
|
301
326
|
on(AdEventType.OPENED, () => ({ type: 'opened' }));
|
|
302
327
|
on(AdEventType.CLICKED, () => ({ type: 'clicked' }));
|
|
303
328
|
on(AdEventType.CLOSED, () => ({ type: 'dismissed' }));
|
|
@@ -312,17 +337,10 @@ export function createAdMobEngine(): AdsEngine {
|
|
|
312
337
|
}
|
|
313
338
|
|
|
314
339
|
on(RewardedAdEventType.LOADED, () => ({ type: 'loaded' }));
|
|
315
|
-
on(RewardedAdEventType.EARNED_REWARD, payload => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
type: 'rewarded',
|
|
320
|
-
reward: {
|
|
321
|
-
type: typeof source.type === 'string' ? source.type : 'reward',
|
|
322
|
-
amount: Number.isFinite(amount) ? amount : 0,
|
|
323
|
-
},
|
|
324
|
-
};
|
|
325
|
-
});
|
|
340
|
+
on(RewardedAdEventType.EARNED_REWARD, payload => ({
|
|
341
|
+
type: 'rewarded',
|
|
342
|
+
reward: normalizeReward(payload),
|
|
343
|
+
}));
|
|
326
344
|
|
|
327
345
|
return makeUnsubscribe(removers);
|
|
328
346
|
},
|
|
@@ -397,3 +415,5 @@ function createAdMobConsent(getProvider: () => ProviderModule | null): ConsentPo
|
|
|
397
415
|
},
|
|
398
416
|
};
|
|
399
417
|
}
|
|
418
|
+
|
|
419
|
+
export { classifyProviderFailure } from './admobErrors';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AdMob's error vocabulary, translated into the port's neutral failure kinds
|
|
3
|
+
* (ADS-08). Shared by the full-screen engine and the banner component, which
|
|
4
|
+
* are the only two places that see a provider error.
|
|
5
|
+
*/
|
|
6
|
+
import type { EngineFailureKind } from './port';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The bridge's error codes that say the provider refused the request itself, so
|
|
10
|
+
* that asking again cannot help (ADS-08). Kept to codes the bridge has reported
|
|
11
|
+
* the same way across releases. `internal-error` is deliberately absent: AdMob
|
|
12
|
+
* uses it for an invalid server response too, which a later request can survive.
|
|
13
|
+
*/
|
|
14
|
+
const REJECTED_CODES: ReadonlySet<string> = new Set([
|
|
15
|
+
'invalid-request',
|
|
16
|
+
'invalid-argument',
|
|
17
|
+
'app-id-missing',
|
|
18
|
+
'request-id-mismatch',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
/** The bridge's code for an error: `error.code`, or the `[googleMobileAds/<code>]` prefix. */
|
|
22
|
+
function providerCodeOf(cause: unknown): string | null {
|
|
23
|
+
const code = (cause as { code?: unknown } | null)?.code;
|
|
24
|
+
if (typeof code === 'string') {
|
|
25
|
+
return code.replace(/^googleMobileAds\//, '');
|
|
26
|
+
}
|
|
27
|
+
const message = cause instanceof Error ? cause.message : typeof cause === 'string' ? cause : '';
|
|
28
|
+
return /\[googleMobileAds\/([a-z-]+)\]/.exec(message)?.[1] ?? null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Classifies a provider failure in neutral terms; undefined when it cannot tell. */
|
|
32
|
+
export function classifyProviderFailure(cause: unknown): EngineFailureKind | undefined {
|
|
33
|
+
const code = providerCodeOf(cause);
|
|
34
|
+
if (code === null) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (code === 'no-fill' || code === 'mediation-no-fill') {
|
|
38
|
+
return 'no_fill';
|
|
39
|
+
}
|
|
40
|
+
if (code === 'network-error') {
|
|
41
|
+
return 'network';
|
|
42
|
+
}
|
|
43
|
+
return REJECTED_CODES.has(code) ? 'rejected' : undefined;
|
|
44
|
+
}
|
package/src/engine/port.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* pretend a banner is a load-then-show resource.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { ComponentType } from 'react';
|
|
16
|
+
|
|
15
17
|
import type { AdContentRating, FullScreenFormat, Reward, Unsubscribe } from '../config/types';
|
|
16
18
|
|
|
17
19
|
/** Privacy state as the engine needs it — no consumer-facing shape leaks in. */
|
|
@@ -39,9 +41,45 @@ export interface EngineRequestOptions {
|
|
|
39
41
|
* `rewarded` and `dismissed` are separate members because they are separate
|
|
40
42
|
* facts that can arrive in either order (FR-049).
|
|
41
43
|
*/
|
|
44
|
+
/**
|
|
45
|
+
* What kind of failure the provider reported, in words that name no provider
|
|
46
|
+
* (ADS-08). Only the adapter knows its provider's error vocabulary, so it
|
|
47
|
+
* classifies; core decides what each kind means for retrying.
|
|
48
|
+
*
|
|
49
|
+
* - `no_fill`: the network had nothing to give.
|
|
50
|
+
* - `network`: the request did not get through.
|
|
51
|
+
* - `rejected`: the provider refused the request itself — an invalid ad unit, a
|
|
52
|
+
* missing app id. The same request will fail the same way.
|
|
53
|
+
*/
|
|
54
|
+
export type EngineFailureKind = 'no_fill' | 'network' | 'rejected';
|
|
55
|
+
|
|
56
|
+
/** What an engine's banner component receives. Names no provider (ADS-12). */
|
|
57
|
+
export interface EngineBannerProps {
|
|
58
|
+
readonly adUnitId: string;
|
|
59
|
+
/** False asks the provider for non-personalized ads only (FR-017). */
|
|
60
|
+
readonly personalizedAds: boolean;
|
|
61
|
+
readonly onLoaded: () => void;
|
|
62
|
+
readonly onFailed: (cause: unknown, kind?: EngineFailureKind) => void;
|
|
63
|
+
/** A real provider impression, as opposed to the derived full-screen one (R5). */
|
|
64
|
+
readonly onImpression: () => void;
|
|
65
|
+
readonly onClicked: () => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** An engine's banner: one adaptive banner filling its container's width (FR-038). */
|
|
69
|
+
export interface EngineBanner {
|
|
70
|
+
/** Whether the provider's banner can render here, e.g. its native module is linked. */
|
|
71
|
+
isAvailable(): boolean;
|
|
72
|
+
readonly Component: ComponentType<EngineBannerProps>;
|
|
73
|
+
}
|
|
74
|
+
|
|
42
75
|
export type EngineAdEvent =
|
|
43
76
|
| { readonly type: 'loaded' }
|
|
44
|
-
| {
|
|
77
|
+
| {
|
|
78
|
+
readonly type: 'failed';
|
|
79
|
+
readonly cause: unknown;
|
|
80
|
+
/** Optional, so an engine that does not classify keeps working. */
|
|
81
|
+
readonly kind?: EngineFailureKind;
|
|
82
|
+
}
|
|
45
83
|
| { readonly type: 'opened' }
|
|
46
84
|
| { readonly type: 'clicked' }
|
|
47
85
|
| { readonly type: 'dismissed' }
|
|
@@ -141,4 +179,11 @@ export interface AdsEngine {
|
|
|
141
179
|
|
|
142
180
|
/** The provider's consent platform. Only touched if the app opts in (FR-102). */
|
|
143
181
|
readonly consent: ConsentPort;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The adaptive banner, rendered by `AdsBanner` (ADS-12). Optional, so an
|
|
185
|
+
* engine written against 0.6.0 still satisfies the port; without it banners
|
|
186
|
+
* report `unavailable`.
|
|
187
|
+
*/
|
|
188
|
+
readonly banner?: EngineBanner;
|
|
144
189
|
}
|
|
@@ -54,8 +54,3 @@ export function isOfficialTestAdUnit(adUnitId: string): boolean {
|
|
|
54
54
|
}
|
|
55
55
|
return false;
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
/** Exposed for the package's own tests and for documentation generation. */
|
|
59
|
-
export function allTestAdUnits(): Readonly<Record<AdFormat, string>> {
|
|
60
|
-
return ANDROID_TEST_AD_UNITS;
|
|
61
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -16,6 +16,14 @@
|
|
|
16
16
|
* frequency cap, and no screen name in this package (FR-045).
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import type { AdsConfig } from './config/types';
|
|
20
|
+
import {
|
|
21
|
+
createAds as createInstance,
|
|
22
|
+
type AdsInstance,
|
|
23
|
+
type CreateAdsOptions,
|
|
24
|
+
} from './core/adsInstance';
|
|
25
|
+
import { createAdMobEngine } from './engine/admobEngine';
|
|
26
|
+
|
|
19
27
|
// Configuration and state
|
|
20
28
|
export type {
|
|
21
29
|
AdContentRating,
|
|
@@ -73,7 +81,17 @@ export {
|
|
|
73
81
|
export type { EnablementResult } from './core/enablement';
|
|
74
82
|
|
|
75
83
|
// Instance, lifecycle, and results
|
|
76
|
-
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Creates an ads instance. This is the composition root: core's instance with
|
|
87
|
+
* the AdMob adapter as its default engine, so core/ itself never loads an
|
|
88
|
+
* adapter (ADS-14). The adapter resolves the provider bridge only on first use,
|
|
89
|
+
* so importing the package where there is no adapter, or no bridge, costs
|
|
90
|
+
* nothing and crashes nothing (FR-025, SC-004).
|
|
91
|
+
*/
|
|
92
|
+
export function createAds(config: AdsConfig, options: CreateAdsOptions = {}): AdsInstance {
|
|
93
|
+
return createInstance(config, options, createAdMobEngine);
|
|
94
|
+
}
|
|
77
95
|
export type {
|
|
78
96
|
AdsInstance,
|
|
79
97
|
ConsentApi,
|