@chipmobilesdk/rn-ads 0.3.0 → 0.4.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 +48 -1
- package/README.md +112 -4
- package/package.json +1 -1
- package/skills/sdk-rn-ads/SKILL.md +42 -8
- package/src/config/types.ts +18 -0
- package/src/core/adsInstance.ts +240 -20
- package/src/core/consentFlow.ts +14 -1
- package/src/core/initMachine.ts +13 -0
- package/src/engine/admobEngine.ts +26 -0
- package/src/engine/port.ts +9 -0
- package/src/index.ts +103 -101
- package/src/testing/index.ts +101 -2
package/src/index.ts
CHANGED
|
@@ -1,101 +1,103 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @chipmobilesdk/rn-ads — reusable advertising capability for
|
|
3
|
-
* ChipMobileSdk-family React Native apps.
|
|
4
|
-
*
|
|
5
|
-
* This is the public entrypoint (LIB-003). Modules under `config/`, `core/`,
|
|
6
|
-
* `engine/`, `react/`, and `diagnostics/` are private; import them only through
|
|
7
|
-
* this file. The fake engine lives behind `@chipmobilesdk/rn-ads/testing` so
|
|
8
|
-
* test doubles cannot reach a production bundle.
|
|
9
|
-
*
|
|
10
|
-
* Nothing exported here names Google, AdMob, or any provider type (FR-002).
|
|
11
|
-
* `src/__tests__/public-api.test.ts` asserts that mechanically.
|
|
12
|
-
*
|
|
13
|
-
* The package owns capability: a safe, consistent way to request and present
|
|
14
|
-
* ads. The consuming app owns policy — where ads appear, when, how often, under
|
|
15
|
-
* what consent, and whether they appear at all. There is no placement rule, no
|
|
16
|
-
* frequency cap, and no screen name in this package (FR-045).
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
// Configuration and state
|
|
20
|
-
export type {
|
|
21
|
-
AdContentRating,
|
|
22
|
-
AdFormat,
|
|
23
|
-
AdsConfig,
|
|
24
|
-
ConsentSnapshot,
|
|
25
|
-
ConsentState,
|
|
26
|
-
ConsentTestingOptions,
|
|
27
|
-
Enablement,
|
|
28
|
-
Environment,
|
|
29
|
-
FullScreenFormat,
|
|
30
|
-
InitStatus,
|
|
31
|
-
PlacementDefinition,
|
|
32
|
-
PlacementState,
|
|
33
|
-
PrivacyBlockReason,
|
|
34
|
-
PrivacyOptions,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
export
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
export {
|
|
89
|
-
export type {
|
|
90
|
-
export {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
export
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @chipmobilesdk/rn-ads — reusable advertising capability for
|
|
3
|
+
* ChipMobileSdk-family React Native apps.
|
|
4
|
+
*
|
|
5
|
+
* This is the public entrypoint (LIB-003). Modules under `config/`, `core/`,
|
|
6
|
+
* `engine/`, `react/`, and `diagnostics/` are private; import them only through
|
|
7
|
+
* this file. The fake engine lives behind `@chipmobilesdk/rn-ads/testing` so
|
|
8
|
+
* test doubles cannot reach a production bundle.
|
|
9
|
+
*
|
|
10
|
+
* Nothing exported here names Google, AdMob, or any provider type (FR-002).
|
|
11
|
+
* `src/__tests__/public-api.test.ts` asserts that mechanically.
|
|
12
|
+
*
|
|
13
|
+
* The package owns capability: a safe, consistent way to request and present
|
|
14
|
+
* ads. The consuming app owns policy — where ads appear, when, how often, under
|
|
15
|
+
* what consent, and whether they appear at all. There is no placement rule, no
|
|
16
|
+
* frequency cap, and no screen name in this package (FR-045).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Configuration and state
|
|
20
|
+
export type {
|
|
21
|
+
AdContentRating,
|
|
22
|
+
AdFormat,
|
|
23
|
+
AdsConfig,
|
|
24
|
+
ConsentSnapshot,
|
|
25
|
+
ConsentState,
|
|
26
|
+
ConsentTestingOptions,
|
|
27
|
+
Enablement,
|
|
28
|
+
Environment,
|
|
29
|
+
FullScreenFormat,
|
|
30
|
+
InitStatus,
|
|
31
|
+
PlacementDefinition,
|
|
32
|
+
PlacementState,
|
|
33
|
+
PrivacyBlockReason,
|
|
34
|
+
PrivacyOptions,
|
|
35
|
+
PrivacyOptionsRequirement,
|
|
36
|
+
RetryPolicy,
|
|
37
|
+
Reward,
|
|
38
|
+
TimeoutConfig,
|
|
39
|
+
UnavailableReason,
|
|
40
|
+
Unsubscribe,
|
|
41
|
+
} from './config/types';
|
|
42
|
+
export { isFullScreenFormat } from './config/types';
|
|
43
|
+
|
|
44
|
+
// Timeout policy — exported so a consumer can read the defaults it is bound by
|
|
45
|
+
// rather than discovering them from a timeout in production (FR-032).
|
|
46
|
+
export {
|
|
47
|
+
AD_VALIDITY_BOUNDS,
|
|
48
|
+
APP_OPEN_VALIDITY_BOUNDS,
|
|
49
|
+
DEFAULT_AD_VALIDITY_MS,
|
|
50
|
+
DEFAULT_APP_OPEN_VALIDITY_MS,
|
|
51
|
+
DEFAULT_LOAD_TIMEOUT_MS,
|
|
52
|
+
DEFAULT_RETRY_POLICY,
|
|
53
|
+
DEFAULT_SHOW_TIMEOUT_MS,
|
|
54
|
+
DEFAULT_UNDECIDED_WARNING_MS,
|
|
55
|
+
LOAD_TIMEOUT_BOUNDS,
|
|
56
|
+
RETRY_BOUNDS,
|
|
57
|
+
SHOW_TIMEOUT_BOUNDS,
|
|
58
|
+
UNDECIDED_WARNING_BOUNDS,
|
|
59
|
+
} from './core/timers';
|
|
60
|
+
|
|
61
|
+
// Request throttle — account safety, added in 0.3.0 (FR-140). Exported so an app
|
|
62
|
+
// can read the floor it is bound by, and so the format-aware default is legible
|
|
63
|
+
// rather than something to discover from a `throttled` state in production.
|
|
64
|
+
export {
|
|
65
|
+
DEFAULT_BANNER_MIN_REQUEST_INTERVAL_MS,
|
|
66
|
+
DEFAULT_FULL_SCREEN_MIN_REQUEST_INTERVAL_MS,
|
|
67
|
+
MIN_REQUEST_INTERVAL_BOUNDS,
|
|
68
|
+
defaultMinRequestIntervalMs,
|
|
69
|
+
} from './core/requestThrottle';
|
|
70
|
+
|
|
71
|
+
// Enablement. `EnablementResult` is what `setEnabled` returns in place of
|
|
72
|
+
// 0.1.0's `void` — the defect that let a re-enable fail in silence (FR-122).
|
|
73
|
+
export type { EnablementResult } from './core/enablement';
|
|
74
|
+
|
|
75
|
+
// Instance, lifecycle, and results
|
|
76
|
+
export { createAds } from './core/adsInstance';
|
|
77
|
+
export type {
|
|
78
|
+
AdsInstance,
|
|
79
|
+
ConsentApi,
|
|
80
|
+
InitResult,
|
|
81
|
+
LoadResult,
|
|
82
|
+
PrivacyOptionsFormResult,
|
|
83
|
+
PrivacyUpdateResult,
|
|
84
|
+
ShowResult,
|
|
85
|
+
} from './core/adsInstance';
|
|
86
|
+
|
|
87
|
+
// React surface
|
|
88
|
+
export { AdsBanner } from './react/AdsBanner';
|
|
89
|
+
export type { AdsBannerProps } from './react/AdsBanner';
|
|
90
|
+
export { AdsProvider, useAds } from './react/AdsProvider';
|
|
91
|
+
export type { AdsProviderProps } from './react/AdsProvider';
|
|
92
|
+
export { useAdsEvents, useAdsStatus, usePlacementState } from './react/hooks';
|
|
93
|
+
|
|
94
|
+
// Diagnostics
|
|
95
|
+
export { AdsConfigError, AdsError, isAdsError } from './diagnostics/errors';
|
|
96
|
+
export type { AdsErrorCode, AdsOperation } from './diagnostics/errors';
|
|
97
|
+
export type {
|
|
98
|
+
AdEvent,
|
|
99
|
+
AdEventListener,
|
|
100
|
+
AdEventType,
|
|
101
|
+
ConfigWarning,
|
|
102
|
+
ImpressionSource,
|
|
103
|
+
} from './diagnostics/events';
|
package/src/testing/index.ts
CHANGED
|
@@ -97,9 +97,27 @@ export function createTestClock(startMs = 1_000_000): TestClock {
|
|
|
97
97
|
export type FakeConsentCall =
|
|
98
98
|
| { readonly kind: 'requestInfoUpdate'; readonly options: EngineConsentOptions }
|
|
99
99
|
| { readonly kind: 'showFormIfRequired' }
|
|
100
|
+
| { readonly kind: 'showPrivacyOptionsForm' }
|
|
100
101
|
| { readonly kind: 'getInfo' }
|
|
101
102
|
| { readonly kind: 'reset' };
|
|
102
103
|
|
|
104
|
+
/**
|
|
105
|
+
* A privacy options form the test is holding open (0.4.0).
|
|
106
|
+
*
|
|
107
|
+
* Settle it whenever the test chooses, which is what lets `busy`, a second
|
|
108
|
+
* call, a consent update, and `dispose()` all be exercised *while* the form is
|
|
109
|
+
* on screen — in any order, rather than only the one a resolved promise allows.
|
|
110
|
+
*/
|
|
111
|
+
export interface HeldPrivacyOptionsForm {
|
|
112
|
+
/**
|
|
113
|
+
* The user closes the form. `info` is what the provider reports afterwards,
|
|
114
|
+
* merged over the current information; omit it for a user who changed nothing.
|
|
115
|
+
*/
|
|
116
|
+
close(info?: Partial<EngineConsentInfo>): void;
|
|
117
|
+
/** The form fails while open. */
|
|
118
|
+
fail(cause?: unknown): void;
|
|
119
|
+
}
|
|
120
|
+
|
|
103
121
|
export type FakeEngineCall =
|
|
104
122
|
| { readonly kind: 'initialize'; readonly privacy: EnginePrivacy }
|
|
105
123
|
| {
|
|
@@ -133,6 +151,7 @@ const DEFAULT_CONSENT: EngineConsentInfo = {
|
|
|
133
151
|
status: 'not_required',
|
|
134
152
|
canRequestAds: true,
|
|
135
153
|
isFormAvailable: false,
|
|
154
|
+
privacyOptions: 'not_required',
|
|
136
155
|
};
|
|
137
156
|
|
|
138
157
|
export interface FakeAdsEngine extends AdsEngine {
|
|
@@ -166,6 +185,19 @@ export interface FakeAdsEngine extends AdsEngine {
|
|
|
166
185
|
failConsentRequest(cause?: unknown): void;
|
|
167
186
|
/** Makes `showFormIfRequired` reject — dismissal is not consent (FR-106). */
|
|
168
187
|
failConsentForm(cause?: unknown): void;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* What the privacy options form reports when the user closes it, merged over
|
|
191
|
+
* the current information. Unset, the form closes with nothing changed — the
|
|
192
|
+
* same as a user who opens it and backs out.
|
|
193
|
+
*/
|
|
194
|
+
setPrivacyOptionsFormResult(info: Partial<EngineConsentInfo>): void;
|
|
195
|
+
/** Makes `showPrivacyOptionsForm` reject — a failure is never consent (FR-106). */
|
|
196
|
+
failPrivacyOptionsForm(cause?: unknown): void;
|
|
197
|
+
/** Holds the next privacy options form open until the test settles it. */
|
|
198
|
+
holdPrivacyOptionsForm(): HeldPrivacyOptionsForm;
|
|
199
|
+
/** Makes `getInfo` reject, for the case where even the re-read after a failed form fails. */
|
|
200
|
+
failConsentInfoRead(cause?: unknown): void;
|
|
169
201
|
}
|
|
170
202
|
|
|
171
203
|
export function createFakeAdsEngine(options: FakeEngineOptions = {}): FakeAdsEngine {
|
|
@@ -175,6 +207,11 @@ export function createFakeAdsEngine(options: FakeEngineOptions = {}): FakeAdsEng
|
|
|
175
207
|
let consentInfo: EngineConsentInfo = { ...DEFAULT_CONSENT, ...options.consentInfo };
|
|
176
208
|
let consentRequestFailure: unknown;
|
|
177
209
|
let consentFormFailure: unknown;
|
|
210
|
+
let privacyFormFailure: unknown;
|
|
211
|
+
let privacyFormResult: Partial<EngineConsentInfo> = {};
|
|
212
|
+
let consentReadFailure: unknown;
|
|
213
|
+
/** Set by `holdPrivacyOptionsForm`; consumed by the next form. */
|
|
214
|
+
let heldForm: Promise<Partial<EngineConsentInfo>> | null = null;
|
|
178
215
|
|
|
179
216
|
const consent: ConsentPort = {
|
|
180
217
|
async requestInfoUpdate(consentOptions) {
|
|
@@ -192,18 +229,49 @@ export function createFakeAdsEngine(options: FakeEngineOptions = {}): FakeAdsEng
|
|
|
192
229
|
// A presented form that the user completes moves the status on. The
|
|
193
230
|
// fake only does this when a form was actually required, mirroring the
|
|
194
231
|
// provider: showing nothing cannot change an answer.
|
|
232
|
+
// A region that required a form is one that requires a way back into
|
|
233
|
+
// it, which is what the provider reports once consent is obtained.
|
|
195
234
|
if (consentInfo.status === 'required' && consentInfo.isFormAvailable) {
|
|
196
|
-
consentInfo = {
|
|
235
|
+
consentInfo = {
|
|
236
|
+
status: 'obtained',
|
|
237
|
+
canRequestAds: true,
|
|
238
|
+
isFormAvailable: false,
|
|
239
|
+
privacyOptions: 'required',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
return consentInfo;
|
|
243
|
+
},
|
|
244
|
+
async showPrivacyOptionsForm() {
|
|
245
|
+
consentCalls.push({ kind: 'showPrivacyOptionsForm' });
|
|
246
|
+
const held = heldForm;
|
|
247
|
+
heldForm = null;
|
|
248
|
+
if (held) {
|
|
249
|
+
// Rejects if the test calls `fail`, which is exactly the provider's
|
|
250
|
+
// failure path; resolves with what the user left behind otherwise.
|
|
251
|
+
consentInfo = { ...consentInfo, ...(await held) };
|
|
252
|
+
return consentInfo;
|
|
197
253
|
}
|
|
254
|
+
if (privacyFormFailure !== undefined) {
|
|
255
|
+
throw privacyFormFailure;
|
|
256
|
+
}
|
|
257
|
+
consentInfo = { ...consentInfo, ...privacyFormResult };
|
|
198
258
|
return consentInfo;
|
|
199
259
|
},
|
|
200
260
|
async getInfo() {
|
|
201
261
|
consentCalls.push({ kind: 'getInfo' });
|
|
262
|
+
if (consentReadFailure !== undefined) {
|
|
263
|
+
throw consentReadFailure;
|
|
264
|
+
}
|
|
202
265
|
return consentInfo;
|
|
203
266
|
},
|
|
204
267
|
reset() {
|
|
205
268
|
consentCalls.push({ kind: 'reset' });
|
|
206
|
-
consentInfo = {
|
|
269
|
+
consentInfo = {
|
|
270
|
+
status: 'unknown',
|
|
271
|
+
canRequestAds: false,
|
|
272
|
+
isFormAvailable: true,
|
|
273
|
+
privacyOptions: 'unknown',
|
|
274
|
+
};
|
|
207
275
|
},
|
|
208
276
|
};
|
|
209
277
|
const listeners = new Map<string, Set<(event: EngineAdEvent) => void>>();
|
|
@@ -238,6 +306,37 @@ export function createFakeAdsEngine(options: FakeEngineOptions = {}): FakeAdsEng
|
|
|
238
306
|
consentFormFailure = cause;
|
|
239
307
|
},
|
|
240
308
|
|
|
309
|
+
setPrivacyOptionsFormResult(info) {
|
|
310
|
+
privacyFormResult = info;
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
failPrivacyOptionsForm(cause = new Error('privacy options form failed')) {
|
|
314
|
+
privacyFormFailure = cause;
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
holdPrivacyOptionsForm() {
|
|
318
|
+
let settle!: {
|
|
319
|
+
resolve: (info: Partial<EngineConsentInfo>) => void;
|
|
320
|
+
reject: (cause: unknown) => void;
|
|
321
|
+
};
|
|
322
|
+
const promise = new Promise<Partial<EngineConsentInfo>>((resolve, reject) => {
|
|
323
|
+
settle = { resolve, reject };
|
|
324
|
+
});
|
|
325
|
+
heldForm = promise;
|
|
326
|
+
return {
|
|
327
|
+
close(info = {}) {
|
|
328
|
+
settle.resolve(info);
|
|
329
|
+
},
|
|
330
|
+
fail(cause = new Error('privacy options form failed')) {
|
|
331
|
+
settle.reject(cause);
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
failConsentInfoRead(cause = new Error('consent information read failed')) {
|
|
337
|
+
consentReadFailure = cause;
|
|
338
|
+
},
|
|
339
|
+
|
|
241
340
|
isAvailable() {
|
|
242
341
|
return options.available !== false;
|
|
243
342
|
},
|