@chipmobilesdk/rn-ads 0.5.0 → 0.5.1
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 +224 -214
- package/README.md +884 -881
- package/package.json +46 -49
- package/skills/sdk-rn-ads/SKILL.md +273 -274
- package/src/config/resolveAdUnit.ts +85 -85
- package/src/config/types.ts +318 -321
- package/src/config/validate.ts +1 -4
- package/src/core/adOriginatedReturn.ts +134 -134
- package/src/core/adsInstance.ts +1929 -1933
- package/src/core/consentFlow.ts +182 -180
- package/src/core/enablement.ts +76 -76
- package/src/core/fullScreenLock.ts +54 -54
- package/src/core/gates.ts +94 -91
- package/src/core/initMachine.ts +187 -187
- package/src/core/placementMachine.ts +124 -124
- package/src/core/privacyGate.ts +93 -94
- package/src/core/retryPolicy.ts +127 -127
- package/src/core/rewardedSession.ts +84 -84
- package/src/core/timers.ts +216 -208
- package/src/diagnostics/correlation.ts +31 -31
- package/src/diagnostics/errors.ts +230 -234
- package/src/diagnostics/events.ts +238 -232
- package/src/engine/admobEngine.ts +399 -404
- package/src/engine/port.ts +144 -144
- package/src/engine/testAdUnits.ts +61 -61
- package/src/react/AdsBanner.tsx +337 -340
- package/src/react/AdsProvider.tsx +57 -57
- package/src/react/hooks.ts +75 -75
- package/src/testing/index.ts +463 -463
package/src/config/types.ts
CHANGED
|
@@ -1,321 +1,318 @@
|
|
|
1
|
-
import type { AdsError } from '../diagnostics/errors';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Public configuration and state types (FR-001, FR-003, FR-004).
|
|
5
|
-
*
|
|
6
|
-
* Nothing here names a platform or an ad provider. That is the whole point:
|
|
7
|
-
* adding an iOS adapter later contributes an optional `iosAdUnitId` and a second
|
|
8
|
-
* engine implementation, and changes nothing a consumer already wrote (FR-006).
|
|
9
|
-
*
|
|
10
|
-
* The package persists none of this. Every value below lives for the lifetime of
|
|
11
|
-
* one instance and is released by `dispose()`.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Supported formats. Native and rewarded-interstitial remain deferred.
|
|
16
|
-
*
|
|
17
|
-
* `app_open` joined in 0.2.0. It is a member of this union rather than a
|
|
18
|
-
* separate concept because FR-132 forbids a parallel vocabulary: app-open uses
|
|
19
|
-
* the same placement states, events, and error codes as everything else. The
|
|
20
|
-
* cost is that a consumer's exhaustive `switch` over this union stops compiling
|
|
21
|
-
* — the one required migration step, documented in the 0.2.0 release notes.
|
|
22
|
-
*/
|
|
23
|
-
export type AdFormat = 'banner' | 'interstitial' | 'rewarded' | 'app_open';
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Formats that take over the screen, and so contend for the single-show lock.
|
|
27
|
-
* App-open is included deliberately: two full-screen ads at once is a broken
|
|
28
|
-
* app regardless of which formats produced them (FR-129).
|
|
29
|
-
*/
|
|
30
|
-
export type FullScreenFormat = Extract<AdFormat, 'interstitial' | 'rewarded' | 'app_open'>;
|
|
31
|
-
|
|
32
|
-
export function isFullScreenFormat(format: AdFormat): format is FullScreenFormat {
|
|
33
|
-
return format === 'interstitial' || format === 'rewarded' || format === 'app_open';
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Declared by the app, because the package cannot depend on any particular
|
|
38
|
-
* configuration mechanism (FR-015). Cross-checked against the runtime dev flag
|
|
39
|
-
* so a debug build claiming `production` is caught rather than trusted (FR-012).
|
|
40
|
-
*/
|
|
41
|
-
export type Environment = 'development' | 'test' | 'production';
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* One logical ad slot. `key` is the app's own stable name — `home_footer`, not
|
|
45
|
-
* an AdMob unit. Provider identifiers appear here in configuration and nowhere
|
|
46
|
-
* else in consumer code (FR-003, FR-007).
|
|
47
|
-
*/
|
|
48
|
-
export interface PlacementDefinition {
|
|
49
|
-
readonly key: string;
|
|
50
|
-
readonly format: AdFormat;
|
|
51
|
-
readonly androidAdUnitId: string;
|
|
52
|
-
/**
|
|
53
|
-
* Additive placeholder for FR-006. Unused in v1 and never read by the Android
|
|
54
|
-
* adapter — present so that adding iOS is a configuration change rather than a
|
|
55
|
-
* type change for every existing consumer.
|
|
56
|
-
*/
|
|
57
|
-
readonly iosAdUnitId?: string;
|
|
58
|
-
/**
|
|
59
|
-
* Per-placement retry, overriding `AdsConfig.retry` for this placement only.
|
|
60
|
-
* Absent inherits; `false` turns retry off here while leaving it on elsewhere.
|
|
61
|
-
*
|
|
62
|
-
* An override *replaces* the instance-wide policy rather than merging into
|
|
63
|
-
* it, so the effective policy for a placement is readable from configuration
|
|
64
|
-
* without simulating a merge. Both are held to the same bounds, so an
|
|
65
|
-
* override can narrow behavior but never exceed a ceiling (FR-136a).
|
|
66
|
-
*/
|
|
67
|
-
readonly retry?: RetryPolicy | false;
|
|
68
|
-
/**
|
|
69
|
-
* Minimum milliseconds between two provider requests for this placement
|
|
70
|
-
* (FR-140), added in 0.3.0.
|
|
71
|
-
*
|
|
72
|
-
* Absent takes the format-aware default: 60s for a banner, off for full-screen
|
|
73
|
-
* formats. `0` turns it off here while leaving it on elsewhere.
|
|
74
|
-
*
|
|
75
|
-
* This is an **account-safety** floor, not a frequency cap: it limits how often
|
|
76
|
-
* a placement may *ask* for an ad, never how often the app may *show* one. If
|
|
77
|
-
* you are reaching for this to pace interstitials, that belongs in the app.
|
|
78
|
-
*/
|
|
79
|
-
readonly minRequestIntervalMs?: number;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Bounded retry for failed loads. Off unless `AdsConfig.retry` is supplied —
|
|
84
|
-
* upgrading from 0.1.0 without configuring this issues not one extra request
|
|
85
|
-
* (FR-134).
|
|
86
|
-
*
|
|
87
|
-
* Only errors already flagged `retryable` are retried. Configuration errors,
|
|
88
|
-
* consent errors, an unsupported platform, and a disabled or undecided instance
|
|
89
|
-
* never are (FR-135).
|
|
90
|
-
*
|
|
91
|
-
* Backoff is exponential with a fixed x2 multiplier and no jitter. Jitter exists
|
|
92
|
-
* to desynchronize a fleet hitting one server; a single device retrying its own
|
|
93
|
-
* load has no herd to avoid, and omitting it keeps the attempt count an exact
|
|
94
|
-
* assertion rather than a statistical one.
|
|
95
|
-
*/
|
|
96
|
-
export interface RetryPolicy {
|
|
97
|
-
/** Attempts *after* the first. Default 2, bounds 1-5. */
|
|
98
|
-
readonly maxRetries?: number;
|
|
99
|
-
/** Delay before the first retry. Default 2000ms, bounds 500-30000. */
|
|
100
|
-
readonly initialDelayMs?: number;
|
|
101
|
-
/** Ceiling the doubling cannot pass. Default 30000ms, bounds 1000-60000. */
|
|
102
|
-
readonly maxDelayMs?: number;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Whether the app wants ads, including the case where it does not know yet.
|
|
107
|
-
*
|
|
108
|
-
* The package never resolves `undecided` on the app's behalf and never learns
|
|
109
|
-
* why the app chose what it chose — no entitlement, no receipt, no reason
|
|
110
|
-
* (FR-119, FR-120).
|
|
111
|
-
*/
|
|
112
|
-
export type Enablement = 'enabled' | 'disabled' | 'undecided';
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Tools for exercising the consent flow from a device outside a
|
|
116
|
-
* consent-required region.
|
|
117
|
-
*
|
|
118
|
-
* **Stripped entirely when `environment` is `production`.** That is enforced at
|
|
119
|
-
* configuration validation rather than at each call site, so no path through the
|
|
120
|
-
* consent module can forget it, and the fact that they were ignored is
|
|
121
|
-
* observable through a `config_warning` event and `getIgnoredConsentTools()` —
|
|
122
|
-
* not merely documented (FR-113).
|
|
123
|
-
*/
|
|
124
|
-
export interface ConsentTestingOptions {
|
|
125
|
-
/** Makes the flow behave as though the device were in this region. */
|
|
126
|
-
readonly debugGeography?: 'disabled' | 'eea' | 'not_eea' | 'regulated_us_state' | 'other';
|
|
127
|
-
/** Clears any stored decision so the flow runs from the beginning. */
|
|
128
|
-
readonly resetOnStart?: boolean;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Consent status, normalized. Carries no provider type and no payload (FR-104).
|
|
133
|
-
*
|
|
134
|
-
* Shaped so an iOS adapter can later express both a consent flow and the
|
|
135
|
-
* platform's tracking-permission mechanism, plus the ordering between them,
|
|
136
|
-
* without changing shape.
|
|
137
|
-
*/
|
|
138
|
-
export type ConsentState =
|
|
139
|
-
/** Not yet asked. The initial value, and the value after a reset. */
|
|
140
|
-
| 'undetermined'
|
|
141
|
-
/** The provider reports this region does not require consent. */
|
|
142
|
-
| 'not_required'
|
|
143
|
-
/** Required and not yet obtained. */
|
|
144
|
-
| 'required'
|
|
145
|
-
/** Obtained. */
|
|
146
|
-
| 'obtained'
|
|
147
|
-
/**
|
|
148
|
-
* The flow failed — a provider error, a network failure, a cancellation, or
|
|
149
|
-
* a form dismissed without an answer. Never, under any circumstance, read as
|
|
150
|
-
* consent granted (FR-106).
|
|
151
|
-
*/
|
|
152
|
-
| 'error';
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Whether the user must be offered a way to change their consent choice
|
|
156
|
-
* (FR-150), added in 0.4.0.
|
|
157
|
-
*
|
|
158
|
-
* The provider's verdict, forwarded — never the package's judgement about a
|
|
159
|
-
* region, exactly as `not_required` in `ConsentState` is not. `required` is the
|
|
160
|
-
* app's cue to show its own "Privacy options" entry; where that entry lives and
|
|
161
|
-
* what it says stay the app's.
|
|
162
|
-
*
|
|
163
|
-
* `unknown` before the first successful `requestInfoUpdate()`, after `reset()`,
|
|
164
|
-
* after a failed update, and on a platform with no adapter. It is not `not_required`:
|
|
165
|
-
* an app that hides the entry on `unknown` is right to, but it must not conclude
|
|
166
|
-
* the user has no right to one.
|
|
167
|
-
*/
|
|
168
|
-
export type PrivacyOptionsRequirement = 'required' | 'not_required' | 'unknown';
|
|
169
|
-
|
|
170
|
-
export interface ConsentSnapshot {
|
|
171
|
-
readonly state: ConsentState;
|
|
172
|
-
/** Always false when `state` is `error`. */
|
|
173
|
-
readonly canRequestAds: boolean;
|
|
174
|
-
readonly isFormAvailable: boolean;
|
|
175
|
-
/** Whether to offer a "change my choice" entry (FR-150). Added in 0.4.0. */
|
|
176
|
-
readonly privacyOptionsRequirement: PrivacyOptionsRequirement;
|
|
177
|
-
readonly error: AdsError | null;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Bounds exist because an unbounded timeout is the same defect as no timeout. */
|
|
181
|
-
export interface TimeoutConfig {
|
|
182
|
-
readonly load?: number;
|
|
183
|
-
readonly show?: number;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export interface AdsConfig {
|
|
187
|
-
/**
|
|
188
|
-
* FR-009. When `false`, every operation is a safe no-op reporting `disabled`.
|
|
189
|
-
*
|
|
190
|
-
* `'undecided'` was added in 0.2.0 for an app that cannot answer this at
|
|
191
|
-
* construction time — one selling an ad-removal tier has to ask a store
|
|
192
|
-
* first. Both boolean spellings keep their exact 0.1.0 meaning; widening an
|
|
193
|
-
* *input* union cannot break a consumer, because consumers write this value
|
|
194
|
-
* rather than switching on it (FR-114, BC-003).
|
|
195
|
-
*/
|
|
196
|
-
readonly enabled: boolean | 'undecided';
|
|
197
|
-
readonly environment: Environment;
|
|
198
|
-
readonly placements: readonly PlacementDefinition[];
|
|
199
|
-
readonly timeouts?: TimeoutConfig;
|
|
200
|
-
/** How long a loaded full-screen ad stays presentable (FR-034). */
|
|
201
|
-
readonly adValidityMs?: number;
|
|
202
|
-
/** Ignored entirely in `production` (FR-014). */
|
|
203
|
-
readonly testDeviceIds?: readonly string[];
|
|
204
|
-
/**
|
|
205
|
-
* How long a loaded app-open ad stays presentable. Separate from
|
|
206
|
-
* `adValidityMs`, which continues to govern interstitial and rewarded.
|
|
207
|
-
*
|
|
208
|
-
* Default 30 minutes, half the other formats' window. Note this is *our*
|
|
209
|
-
* constraint, not the provider's — Google expires app-open ads after four
|
|
210
|
-
* hours, which is longer than this package's general full-screen window. The
|
|
211
|
-
* shorter window trades some fill for a fresher creative at the most
|
|
212
|
-
* prominent moment in the app (FR-130).
|
|
213
|
-
*/
|
|
214
|
-
readonly appOpenValidityMs?: number;
|
|
215
|
-
/**
|
|
216
|
-
* How long `enabled: 'undecided'` may go unresolved before the package emits
|
|
217
|
-
* one diagnostic warning. Default 10 seconds.
|
|
218
|
-
*
|
|
219
|
-
* It warns and does nothing else. Resolving on the app's behalf would mean
|
|
220
|
-
* guessing at exactly the business decision FR-120 forbids the package from
|
|
221
|
-
* knowing.
|
|
222
|
-
*/
|
|
223
|
-
readonly undecidedWarningMs?: number;
|
|
224
|
-
/** Instance-wide retry. Absent means retry is entirely off (FR-134). */
|
|
225
|
-
readonly retry?: RetryPolicy;
|
|
226
|
-
/** Consent testing tools. Stripped in `production` (FR-113). */
|
|
227
|
-
readonly consentTesting?: ConsentTestingOptions;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** Content ratings, normalized. Maps onto the provider's own rating scale. */
|
|
231
|
-
export type AdContentRating = 'G' | 'PG' | 'T' | 'MA';
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Supplied before initialization (FR-016). Never logged, never persisted
|
|
235
|
-
* (FR-021). Everything except `canRequestAds` must reach the provider before it
|
|
236
|
-
* initializes, which is why changing one afterwards reports
|
|
237
|
-
* `requires_reinitialization` rather than pretending it applied (FR-020).
|
|
238
|
-
*/
|
|
239
|
-
export interface PrivacyOptions {
|
|
240
|
-
/** The gate itself. False means no request may be made, for any reason. */
|
|
241
|
-
readonly canRequestAds: boolean;
|
|
242
|
-
/** Distinguishes "the user said no" from "we have not asked yet" (FR-018). */
|
|
243
|
-
readonly consentUndetermined?: boolean;
|
|
244
|
-
readonly childDirected?: boolean;
|
|
245
|
-
readonly underAgeOfConsent?: boolean;
|
|
246
|
-
readonly maxAdContentRating?: AdContentRating;
|
|
247
|
-
/** When false, the package requests non-personalized ads. */
|
|
248
|
-
readonly personalizedAds?: boolean;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Why the consent gate is closed.
|
|
253
|
-
*
|
|
254
|
-
* The two original members keep their exact v1 meaning — the app's own gate —
|
|
255
|
-
* so no existing consumer's interpretation changes. The two added in 0.2.0 name
|
|
256
|
-
* the package's consent flow instead. The split exists because the gate has two
|
|
257
|
-
* independent inputs that combine as most-restrictive-wins, and a refusal has to
|
|
258
|
-
* say *which* of them is blocking or the app cannot act on it (FR-105a).
|
|
259
|
-
*/
|
|
260
|
-
export type PrivacyBlockReason =
|
|
261
|
-
| '
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
export type
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
| '
|
|
271
|
-
| '
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
| '
|
|
295
|
-
| '
|
|
296
|
-
| '
|
|
297
|
-
|
|
298
|
-
| '
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
/** FR-005. Returned by every subscription; safe to call more than once. */
|
|
321
|
-
export type Unsubscribe = () => void;
|
|
1
|
+
import type { AdsError } from '../diagnostics/errors';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public configuration and state types (FR-001, FR-003, FR-004).
|
|
5
|
+
*
|
|
6
|
+
* Nothing here names a platform or an ad provider. That is the whole point:
|
|
7
|
+
* adding an iOS adapter later contributes an optional `iosAdUnitId` and a second
|
|
8
|
+
* engine implementation, and changes nothing a consumer already wrote (FR-006).
|
|
9
|
+
*
|
|
10
|
+
* The package persists none of this. Every value below lives for the lifetime of
|
|
11
|
+
* one instance and is released by `dispose()`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Supported formats. Native and rewarded-interstitial remain deferred.
|
|
16
|
+
*
|
|
17
|
+
* `app_open` joined in 0.2.0. It is a member of this union rather than a
|
|
18
|
+
* separate concept because FR-132 forbids a parallel vocabulary: app-open uses
|
|
19
|
+
* the same placement states, events, and error codes as everything else. The
|
|
20
|
+
* cost is that a consumer's exhaustive `switch` over this union stops compiling
|
|
21
|
+
* — the one required migration step, documented in the 0.2.0 release notes.
|
|
22
|
+
*/
|
|
23
|
+
export type AdFormat = 'banner' | 'interstitial' | 'rewarded' | 'app_open';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Formats that take over the screen, and so contend for the single-show lock.
|
|
27
|
+
* App-open is included deliberately: two full-screen ads at once is a broken
|
|
28
|
+
* app regardless of which formats produced them (FR-129).
|
|
29
|
+
*/
|
|
30
|
+
export type FullScreenFormat = Extract<AdFormat, 'interstitial' | 'rewarded' | 'app_open'>;
|
|
31
|
+
|
|
32
|
+
export function isFullScreenFormat(format: AdFormat): format is FullScreenFormat {
|
|
33
|
+
return format === 'interstitial' || format === 'rewarded' || format === 'app_open';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Declared by the app, because the package cannot depend on any particular
|
|
38
|
+
* configuration mechanism (FR-015). Cross-checked against the runtime dev flag
|
|
39
|
+
* so a debug build claiming `production` is caught rather than trusted (FR-012).
|
|
40
|
+
*/
|
|
41
|
+
export type Environment = 'development' | 'test' | 'production';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* One logical ad slot. `key` is the app's own stable name — `home_footer`, not
|
|
45
|
+
* an AdMob unit. Provider identifiers appear here in configuration and nowhere
|
|
46
|
+
* else in consumer code (FR-003, FR-007).
|
|
47
|
+
*/
|
|
48
|
+
export interface PlacementDefinition {
|
|
49
|
+
readonly key: string;
|
|
50
|
+
readonly format: AdFormat;
|
|
51
|
+
readonly androidAdUnitId: string;
|
|
52
|
+
/**
|
|
53
|
+
* Additive placeholder for FR-006. Unused in v1 and never read by the Android
|
|
54
|
+
* adapter — present so that adding iOS is a configuration change rather than a
|
|
55
|
+
* type change for every existing consumer.
|
|
56
|
+
*/
|
|
57
|
+
readonly iosAdUnitId?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Per-placement retry, overriding `AdsConfig.retry` for this placement only.
|
|
60
|
+
* Absent inherits; `false` turns retry off here while leaving it on elsewhere.
|
|
61
|
+
*
|
|
62
|
+
* An override *replaces* the instance-wide policy rather than merging into
|
|
63
|
+
* it, so the effective policy for a placement is readable from configuration
|
|
64
|
+
* without simulating a merge. Both are held to the same bounds, so an
|
|
65
|
+
* override can narrow behavior but never exceed a ceiling (FR-136a).
|
|
66
|
+
*/
|
|
67
|
+
readonly retry?: RetryPolicy | false;
|
|
68
|
+
/**
|
|
69
|
+
* Minimum milliseconds between two provider requests for this placement
|
|
70
|
+
* (FR-140), added in 0.3.0.
|
|
71
|
+
*
|
|
72
|
+
* Absent takes the format-aware default: 60s for a banner, off for full-screen
|
|
73
|
+
* formats. `0` turns it off here while leaving it on elsewhere.
|
|
74
|
+
*
|
|
75
|
+
* This is an **account-safety** floor, not a frequency cap: it limits how often
|
|
76
|
+
* a placement may *ask* for an ad, never how often the app may *show* one. If
|
|
77
|
+
* you are reaching for this to pace interstitials, that belongs in the app.
|
|
78
|
+
*/
|
|
79
|
+
readonly minRequestIntervalMs?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Bounded retry for failed loads. Off unless `AdsConfig.retry` is supplied —
|
|
84
|
+
* upgrading from 0.1.0 without configuring this issues not one extra request
|
|
85
|
+
* (FR-134).
|
|
86
|
+
*
|
|
87
|
+
* Only errors already flagged `retryable` are retried. Configuration errors,
|
|
88
|
+
* consent errors, an unsupported platform, and a disabled or undecided instance
|
|
89
|
+
* never are (FR-135).
|
|
90
|
+
*
|
|
91
|
+
* Backoff is exponential with a fixed x2 multiplier and no jitter. Jitter exists
|
|
92
|
+
* to desynchronize a fleet hitting one server; a single device retrying its own
|
|
93
|
+
* load has no herd to avoid, and omitting it keeps the attempt count an exact
|
|
94
|
+
* assertion rather than a statistical one.
|
|
95
|
+
*/
|
|
96
|
+
export interface RetryPolicy {
|
|
97
|
+
/** Attempts *after* the first. Default 2, bounds 1-5. */
|
|
98
|
+
readonly maxRetries?: number;
|
|
99
|
+
/** Delay before the first retry. Default 2000ms, bounds 500-30000. */
|
|
100
|
+
readonly initialDelayMs?: number;
|
|
101
|
+
/** Ceiling the doubling cannot pass. Default 30000ms, bounds 1000-60000. */
|
|
102
|
+
readonly maxDelayMs?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether the app wants ads, including the case where it does not know yet.
|
|
107
|
+
*
|
|
108
|
+
* The package never resolves `undecided` on the app's behalf and never learns
|
|
109
|
+
* why the app chose what it chose — no entitlement, no receipt, no reason
|
|
110
|
+
* (FR-119, FR-120).
|
|
111
|
+
*/
|
|
112
|
+
export type Enablement = 'enabled' | 'disabled' | 'undecided';
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Tools for exercising the consent flow from a device outside a
|
|
116
|
+
* consent-required region.
|
|
117
|
+
*
|
|
118
|
+
* **Stripped entirely when `environment` is `production`.** That is enforced at
|
|
119
|
+
* configuration validation rather than at each call site, so no path through the
|
|
120
|
+
* consent module can forget it, and the fact that they were ignored is
|
|
121
|
+
* observable through a `config_warning` event and `getIgnoredConsentTools()` —
|
|
122
|
+
* not merely documented (FR-113).
|
|
123
|
+
*/
|
|
124
|
+
export interface ConsentTestingOptions {
|
|
125
|
+
/** Makes the flow behave as though the device were in this region. */
|
|
126
|
+
readonly debugGeography?: 'disabled' | 'eea' | 'not_eea' | 'regulated_us_state' | 'other';
|
|
127
|
+
/** Clears any stored decision so the flow runs from the beginning. */
|
|
128
|
+
readonly resetOnStart?: boolean;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Consent status, normalized. Carries no provider type and no payload (FR-104).
|
|
133
|
+
*
|
|
134
|
+
* Shaped so an iOS adapter can later express both a consent flow and the
|
|
135
|
+
* platform's tracking-permission mechanism, plus the ordering between them,
|
|
136
|
+
* without changing shape.
|
|
137
|
+
*/
|
|
138
|
+
export type ConsentState =
|
|
139
|
+
/** Not yet asked. The initial value, and the value after a reset. */
|
|
140
|
+
| 'undetermined'
|
|
141
|
+
/** The provider reports this region does not require consent. */
|
|
142
|
+
| 'not_required'
|
|
143
|
+
/** Required and not yet obtained. */
|
|
144
|
+
| 'required'
|
|
145
|
+
/** Obtained. */
|
|
146
|
+
| 'obtained'
|
|
147
|
+
/**
|
|
148
|
+
* The flow failed — a provider error, a network failure, a cancellation, or
|
|
149
|
+
* a form dismissed without an answer. Never, under any circumstance, read as
|
|
150
|
+
* consent granted (FR-106).
|
|
151
|
+
*/
|
|
152
|
+
| 'error';
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Whether the user must be offered a way to change their consent choice
|
|
156
|
+
* (FR-150), added in 0.4.0.
|
|
157
|
+
*
|
|
158
|
+
* The provider's verdict, forwarded — never the package's judgement about a
|
|
159
|
+
* region, exactly as `not_required` in `ConsentState` is not. `required` is the
|
|
160
|
+
* app's cue to show its own "Privacy options" entry; where that entry lives and
|
|
161
|
+
* what it says stay the app's.
|
|
162
|
+
*
|
|
163
|
+
* `unknown` before the first successful `requestInfoUpdate()`, after `reset()`,
|
|
164
|
+
* after a failed update, and on a platform with no adapter. It is not `not_required`:
|
|
165
|
+
* an app that hides the entry on `unknown` is right to, but it must not conclude
|
|
166
|
+
* the user has no right to one.
|
|
167
|
+
*/
|
|
168
|
+
export type PrivacyOptionsRequirement = 'required' | 'not_required' | 'unknown';
|
|
169
|
+
|
|
170
|
+
export interface ConsentSnapshot {
|
|
171
|
+
readonly state: ConsentState;
|
|
172
|
+
/** Always false when `state` is `error`. */
|
|
173
|
+
readonly canRequestAds: boolean;
|
|
174
|
+
readonly isFormAvailable: boolean;
|
|
175
|
+
/** Whether to offer a "change my choice" entry (FR-150). Added in 0.4.0. */
|
|
176
|
+
readonly privacyOptionsRequirement: PrivacyOptionsRequirement;
|
|
177
|
+
readonly error: AdsError | null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Bounds exist because an unbounded timeout is the same defect as no timeout. */
|
|
181
|
+
export interface TimeoutConfig {
|
|
182
|
+
readonly load?: number;
|
|
183
|
+
readonly show?: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface AdsConfig {
|
|
187
|
+
/**
|
|
188
|
+
* FR-009. When `false`, every operation is a safe no-op reporting `disabled`.
|
|
189
|
+
*
|
|
190
|
+
* `'undecided'` was added in 0.2.0 for an app that cannot answer this at
|
|
191
|
+
* construction time — one selling an ad-removal tier has to ask a store
|
|
192
|
+
* first. Both boolean spellings keep their exact 0.1.0 meaning; widening an
|
|
193
|
+
* *input* union cannot break a consumer, because consumers write this value
|
|
194
|
+
* rather than switching on it (FR-114, BC-003).
|
|
195
|
+
*/
|
|
196
|
+
readonly enabled: boolean | 'undecided';
|
|
197
|
+
readonly environment: Environment;
|
|
198
|
+
readonly placements: readonly PlacementDefinition[];
|
|
199
|
+
readonly timeouts?: TimeoutConfig;
|
|
200
|
+
/** How long a loaded full-screen ad stays presentable (FR-034). */
|
|
201
|
+
readonly adValidityMs?: number;
|
|
202
|
+
/** Ignored entirely in `production` (FR-014). */
|
|
203
|
+
readonly testDeviceIds?: readonly string[];
|
|
204
|
+
/**
|
|
205
|
+
* How long a loaded app-open ad stays presentable. Separate from
|
|
206
|
+
* `adValidityMs`, which continues to govern interstitial and rewarded.
|
|
207
|
+
*
|
|
208
|
+
* Default 30 minutes, half the other formats' window. Note this is *our*
|
|
209
|
+
* constraint, not the provider's — Google expires app-open ads after four
|
|
210
|
+
* hours, which is longer than this package's general full-screen window. The
|
|
211
|
+
* shorter window trades some fill for a fresher creative at the most
|
|
212
|
+
* prominent moment in the app (FR-130).
|
|
213
|
+
*/
|
|
214
|
+
readonly appOpenValidityMs?: number;
|
|
215
|
+
/**
|
|
216
|
+
* How long `enabled: 'undecided'` may go unresolved before the package emits
|
|
217
|
+
* one diagnostic warning. Default 10 seconds.
|
|
218
|
+
*
|
|
219
|
+
* It warns and does nothing else. Resolving on the app's behalf would mean
|
|
220
|
+
* guessing at exactly the business decision FR-120 forbids the package from
|
|
221
|
+
* knowing.
|
|
222
|
+
*/
|
|
223
|
+
readonly undecidedWarningMs?: number;
|
|
224
|
+
/** Instance-wide retry. Absent means retry is entirely off (FR-134). */
|
|
225
|
+
readonly retry?: RetryPolicy;
|
|
226
|
+
/** Consent testing tools. Stripped in `production` (FR-113). */
|
|
227
|
+
readonly consentTesting?: ConsentTestingOptions;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Content ratings, normalized. Maps onto the provider's own rating scale. */
|
|
231
|
+
export type AdContentRating = 'G' | 'PG' | 'T' | 'MA';
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Supplied before initialization (FR-016). Never logged, never persisted
|
|
235
|
+
* (FR-021). Everything except `canRequestAds` must reach the provider before it
|
|
236
|
+
* initializes, which is why changing one afterwards reports
|
|
237
|
+
* `requires_reinitialization` rather than pretending it applied (FR-020).
|
|
238
|
+
*/
|
|
239
|
+
export interface PrivacyOptions {
|
|
240
|
+
/** The gate itself. False means no request may be made, for any reason. */
|
|
241
|
+
readonly canRequestAds: boolean;
|
|
242
|
+
/** Distinguishes "the user said no" from "we have not asked yet" (FR-018). */
|
|
243
|
+
readonly consentUndetermined?: boolean;
|
|
244
|
+
readonly childDirected?: boolean;
|
|
245
|
+
readonly underAgeOfConsent?: boolean;
|
|
246
|
+
readonly maxAdContentRating?: AdContentRating;
|
|
247
|
+
/** When false, the package requests non-personalized ads. */
|
|
248
|
+
readonly personalizedAds?: boolean;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Why the consent gate is closed.
|
|
253
|
+
*
|
|
254
|
+
* The two original members keep their exact v1 meaning — the app's own gate —
|
|
255
|
+
* so no existing consumer's interpretation changes. The two added in 0.2.0 name
|
|
256
|
+
* the package's consent flow instead. The split exists because the gate has two
|
|
257
|
+
* independent inputs that combine as most-restrictive-wins, and a refusal has to
|
|
258
|
+
* say *which* of them is blocking or the app cannot act on it (FR-105a).
|
|
259
|
+
*/
|
|
260
|
+
export type PrivacyBlockReason =
|
|
261
|
+
'consent_required' | 'consent_undetermined' | 'consent_flow_required' | 'consent_flow_error';
|
|
262
|
+
|
|
263
|
+
export type UnavailableReason = 'unsupported_platform' | 'provider_missing';
|
|
264
|
+
|
|
265
|
+
/** FR-023. One per instance, observable without holding a provider object. */
|
|
266
|
+
export type InitStatus =
|
|
267
|
+
| 'not_started'
|
|
268
|
+
| 'blocked'
|
|
269
|
+
| 'initializing'
|
|
270
|
+
| 'ready'
|
|
271
|
+
| 'disabled'
|
|
272
|
+
/**
|
|
273
|
+
* The app has not decided whether ads are on yet — it is still waiting on
|
|
274
|
+
* whatever answers that question, typically a store entitlement sync.
|
|
275
|
+
*
|
|
276
|
+
* Behaves exactly like `disabled` for requests (zero reach the provider) but
|
|
277
|
+
* is deliberately a separate value. That distinction is the entire point:
|
|
278
|
+
* `disabled` means a settled no, and a banner host can reclaim its space;
|
|
279
|
+
* `undecided` means not yet, and the app may want to hold the space so a
|
|
280
|
+
* paying user never sees an ad flash in and out (FR-116).
|
|
281
|
+
*/
|
|
282
|
+
| 'undecided'
|
|
283
|
+
| 'unavailable'
|
|
284
|
+
| 'failed';
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* FR-026. One per placement. Banners never reach `consumed` — they are
|
|
288
|
+
* continuous, not single-use.
|
|
289
|
+
*/
|
|
290
|
+
export type PlacementState =
|
|
291
|
+
| 'idle'
|
|
292
|
+
| 'loading'
|
|
293
|
+
| 'ready'
|
|
294
|
+
| 'showing'
|
|
295
|
+
| 'consumed'
|
|
296
|
+
| 'failed'
|
|
297
|
+
/** Mirrors `InitStatus.undecided` so the banner host can report it directly. */
|
|
298
|
+
| 'undecided'
|
|
299
|
+
/**
|
|
300
|
+
* Refused by the per-placement request throttle (FR-140), added in 0.3.0.
|
|
301
|
+
*
|
|
302
|
+
* Deliberately distinct from `failed` and from a no-fill: nothing is wrong,
|
|
303
|
+
* nothing was requested, and the same placement will work again shortly. An
|
|
304
|
+
* app that collapses its ad slot on `failed` will usually want to collapse on
|
|
305
|
+
* this too, but it must be able to tell the two apart when diagnosing why a
|
|
306
|
+
* banner is absent.
|
|
307
|
+
*/
|
|
308
|
+
| 'throttled'
|
|
309
|
+
| 'unavailable';
|
|
310
|
+
|
|
311
|
+
/** FR-047. The provider's reward descriptor, normalized. */
|
|
312
|
+
export interface Reward {
|
|
313
|
+
readonly type: string;
|
|
314
|
+
readonly amount: number;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** FR-005. Returned by every subscription; safe to call more than once. */
|
|
318
|
+
export type Unsubscribe = () => void;
|
package/src/config/validate.ts
CHANGED
|
@@ -13,10 +13,7 @@
|
|
|
13
13
|
import { AdsConfigError } from '../diagnostics/errors';
|
|
14
14
|
import { resolvePlacementAdUnits, type ResolvedCatalog } from './resolveAdUnit';
|
|
15
15
|
import { checkBand, resolveTimeouts, type ResolvedTimeouts } from '../core/timers';
|
|
16
|
-
import {
|
|
17
|
-
defaultMinRequestIntervalMs,
|
|
18
|
-
MIN_REQUEST_INTERVAL_BOUNDS,
|
|
19
|
-
} from '../core/requestThrottle';
|
|
16
|
+
import { defaultMinRequestIntervalMs, MIN_REQUEST_INTERVAL_BOUNDS } from '../core/requestThrottle';
|
|
20
17
|
import { toEnablement } from '../core/enablement';
|
|
21
18
|
import { resolveEffectiveRetry, type ResolvedRetryPolicy } from '../core/retryPolicy';
|
|
22
19
|
import type {
|