@chipmobilesdk/rn-ads 0.5.0 → 0.6.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 +351 -214
- package/README.md +891 -881
- package/package.json +49 -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 +4 -5
- package/src/core/adOriginatedReturn.ts +134 -134
- package/src/core/adsInstance.ts +1899 -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 +137 -127
- package/src/core/rewardedSession.ts +84 -84
- package/src/core/timers.ts +164 -208
- package/src/diagnostics/correlation.ts +31 -31
- package/src/diagnostics/errors.ts +200 -234
- package/src/diagnostics/events.ts +249 -232
- package/src/engine/admobEngine.ts +410 -404
- package/src/engine/port.ts +144 -144
- package/src/engine/testAdUnits.ts +61 -61
- package/src/index.ts +19 -1
- package/src/react/AdsBanner.tsx +351 -340
- package/src/react/AdsProvider.tsx +56 -57
- package/src/react/hooks.ts +70 -75
- package/src/testing/conformance.ts +112 -0
- package/src/testing/index.ts +475 -463
package/src/core/timers.ts
CHANGED
|
@@ -1,208 +1,164 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Timeout and staleness policy (FR-032, FR-034).
|
|
3
|
-
*
|
|
4
|
-
* Defaults and bounds live here rather than at each call site so there is one
|
|
5
|
-
* place to read what the package will wait for. Values outside a band are a
|
|
6
|
-
* configuration error rather than being silently clamped: silently clamping a
|
|
7
|
-
* one-second load timeout to five would mean the app is told it configured
|
|
8
|
-
* something it did not (FR-008).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
export const
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
*
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export const
|
|
51
|
-
|
|
52
|
-
export const
|
|
53
|
-
|
|
54
|
-
export const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* transient
|
|
64
|
-
*
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* the
|
|
85
|
-
*
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
export const
|
|
90
|
-
|
|
91
|
-
export
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
readonly
|
|
95
|
-
readonly
|
|
96
|
-
readonly
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
* a leaked timer is otherwise invisible until it fires into a disposed consumer.
|
|
166
|
-
*/
|
|
167
|
-
export interface TimerSet {
|
|
168
|
-
after(ms: number, callback: () => void): Unsubscribe;
|
|
169
|
-
clearAll(): void;
|
|
170
|
-
size(): number;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function createTimerSet(scheduler: Scheduler = realScheduler): TimerSet {
|
|
174
|
-
const handles = new Set<unknown>();
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
after(ms, callback) {
|
|
178
|
-
const handle = scheduler.setTimeout(() => {
|
|
179
|
-
handles.delete(handle);
|
|
180
|
-
callback();
|
|
181
|
-
}, ms);
|
|
182
|
-
handles.add(handle);
|
|
183
|
-
let cleared = false;
|
|
184
|
-
return () => {
|
|
185
|
-
if (cleared) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
cleared = true;
|
|
189
|
-
scheduler.clearTimeout(handle);
|
|
190
|
-
handles.delete(handle);
|
|
191
|
-
};
|
|
192
|
-
},
|
|
193
|
-
clearAll() {
|
|
194
|
-
for (const handle of handles) {
|
|
195
|
-
scheduler.clearTimeout(handle);
|
|
196
|
-
}
|
|
197
|
-
handles.clear();
|
|
198
|
-
},
|
|
199
|
-
size() {
|
|
200
|
-
return handles.size;
|
|
201
|
-
},
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/** A loaded ad past its validity window must never be reported ready (FR-034). */
|
|
206
|
-
export function isStale(loadedAtMs: number, nowMs: number, adValidityMs: number): boolean {
|
|
207
|
-
return nowMs - loadedAtMs >= adValidityMs;
|
|
208
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Timeout and staleness policy (FR-032, FR-034).
|
|
3
|
+
*
|
|
4
|
+
* Defaults and bounds live here rather than at each call site so there is one
|
|
5
|
+
* place to read what the package will wait for. Values outside a band are a
|
|
6
|
+
* configuration error rather than being silently clamped: silently clamping a
|
|
7
|
+
* one-second load timeout to five would mean the app is told it configured
|
|
8
|
+
* something it did not (FR-008).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { checkRange, type Range } from '@chipmobilesdk/rn-core';
|
|
12
|
+
|
|
13
|
+
import { AdsConfigError, type AdsErrorCode } from '../diagnostics/errors';
|
|
14
|
+
import type { TimeoutConfig } from '../config/types';
|
|
15
|
+
|
|
16
|
+
/** Comfortably longer than a normal fill on a slow mobile network, still bounded. */
|
|
17
|
+
export const DEFAULT_LOAD_TIMEOUT_MS = 30_000;
|
|
18
|
+
/** Generous for what is essentially a local presentation. */
|
|
19
|
+
export const DEFAULT_SHOW_TIMEOUT_MS = 10_000;
|
|
20
|
+
/**
|
|
21
|
+
* Google documents that a loaded ad object expires about an hour after load.
|
|
22
|
+
* Sixty minutes is the provider's own number; choosing anything else would be
|
|
23
|
+
* inventing a constraint (R7).
|
|
24
|
+
*/
|
|
25
|
+
export const DEFAULT_AD_VALIDITY_MS = 3_600_000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* App-open's own window, deliberately shorter than the hour the other
|
|
29
|
+
* full-screen formats get (FR-130).
|
|
30
|
+
*
|
|
31
|
+
* Worth being explicit about why: Google expires app-open ads after **four
|
|
32
|
+
* hours**, which is *longer* than DEFAULT_AD_VALIDITY_MS, so the provider does
|
|
33
|
+
* not constrain this at all. The shorter window is a product decision. An
|
|
34
|
+
* app-open ad shows at the most prominent moment the app has, and a creative
|
|
35
|
+
* cached for an hour is stale by then; thirty minutes still spares a returning
|
|
36
|
+
* user the spinner of a cold load, which is both the worst app-open experience
|
|
37
|
+
* and the specific behavior store policy penalizes.
|
|
38
|
+
*/
|
|
39
|
+
export const DEFAULT_APP_OPEN_VALIDITY_MS = 1_800_000;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* How long `enabled: 'undecided'` may go unresolved before one warning fires.
|
|
43
|
+
*
|
|
44
|
+
* Ten seconds is several times a realistic worst case for the round trip this
|
|
45
|
+
* state exists to cover — asking a store whether the user owns an ad-removal
|
|
46
|
+
* entitlement — so an app doing the right thing essentially never trips it,
|
|
47
|
+
* while an app that forgot to resolve trips it inside a single QA session,
|
|
48
|
+
* which is where it needs to be caught (FR-119).
|
|
49
|
+
*/
|
|
50
|
+
export const DEFAULT_UNDECIDED_WARNING_MS = 10_000;
|
|
51
|
+
|
|
52
|
+
export const LOAD_TIMEOUT_BOUNDS = { min: 5_000, max: 120_000 } as const;
|
|
53
|
+
export const SHOW_TIMEOUT_BOUNDS = { min: 5_000, max: 30_000 } as const;
|
|
54
|
+
export const AD_VALIDITY_BOUNDS = { min: 60_000, max: 3_600_000 } as const;
|
|
55
|
+
/** Ceiling is one hour so app-open can never outlive the general window. */
|
|
56
|
+
export const APP_OPEN_VALIDITY_BOUNDS = { min: 60_000, max: 3_600_000 } as const;
|
|
57
|
+
export const UNDECIDED_WARNING_BOUNDS = { min: 1_000, max: 120_000 } as const;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Retry defaults (FR-136).
|
|
61
|
+
*
|
|
62
|
+
* Two retries rather than more: each one is a billable request, and two covers
|
|
63
|
+
* the transient network failure and the momentary no-fill — the cases where
|
|
64
|
+
* trying again plausibly helps. A third mostly pays for a failure that was not
|
|
65
|
+
* transient. 2s then 4s keeps both inside a window the user still associates
|
|
66
|
+
* with the action that triggered the load.
|
|
67
|
+
*/
|
|
68
|
+
export const DEFAULT_RETRY_POLICY = {
|
|
69
|
+
maxRetries: 2,
|
|
70
|
+
initialDelayMs: 2_000,
|
|
71
|
+
maxDelayMs: 30_000,
|
|
72
|
+
} as const;
|
|
73
|
+
|
|
74
|
+
export const RETRY_BOUNDS = {
|
|
75
|
+
maxRetries: { min: 1, max: 5 },
|
|
76
|
+
initialDelayMs: { min: 500, max: 30_000 },
|
|
77
|
+
maxDelayMs: { min: 1_000, max: 60_000 },
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Ad-originated return detection (FR-128).
|
|
82
|
+
*
|
|
83
|
+
* A background transition within CLICK_WINDOW of a click is attributed to a
|
|
84
|
+
* browser the ad opened. An absence longer than MAX_AWAY is no longer
|
|
85
|
+
* attributable to the ad — the user did something else in between. GRACE covers
|
|
86
|
+
* the moment right after the return, where an app-open show would otherwise
|
|
87
|
+
* fire, without outliving it and refusing a genuinely later show.
|
|
88
|
+
*/
|
|
89
|
+
export const AD_CLICK_WINDOW_MS = 2_000;
|
|
90
|
+
export const AD_RETURN_MAX_AWAY_MS = 300_000;
|
|
91
|
+
export const AD_RETURN_GRACE_MS = 10_000;
|
|
92
|
+
|
|
93
|
+
export interface ResolvedTimeouts {
|
|
94
|
+
readonly load: number;
|
|
95
|
+
readonly show: number;
|
|
96
|
+
readonly adValidity: number;
|
|
97
|
+
readonly appOpenValidity: number;
|
|
98
|
+
readonly undecidedWarning: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `value` when it is inside `bounds`, else an `AdsConfigError` with `code`: each
|
|
103
|
+
* field reports the code its documentation names, so a retry policy out of bounds
|
|
104
|
+
* is `retry_out_of_bounds`, not a timeout's code (CORE-04, ADS-16).
|
|
105
|
+
*/
|
|
106
|
+
export function checkBand(
|
|
107
|
+
label: string,
|
|
108
|
+
value: number,
|
|
109
|
+
bounds: Range,
|
|
110
|
+
code: AdsErrorCode = 'timeout_out_of_bounds',
|
|
111
|
+
unit = 'ms',
|
|
112
|
+
): number {
|
|
113
|
+
return checkRange(label, value, bounds, message => new AdsConfigError(code, { message }), unit);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface TimeoutInput {
|
|
117
|
+
readonly timeouts?: TimeoutConfig;
|
|
118
|
+
readonly adValidityMs?: number;
|
|
119
|
+
readonly appOpenValidityMs?: number;
|
|
120
|
+
readonly undecidedWarningMs?: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function resolveTimeouts(input: TimeoutInput): ResolvedTimeouts {
|
|
124
|
+
const { timeouts, adValidityMs, appOpenValidityMs, undecidedWarningMs } = input;
|
|
125
|
+
return {
|
|
126
|
+
load: checkBand(
|
|
127
|
+
'timeouts.load',
|
|
128
|
+
timeouts?.load ?? DEFAULT_LOAD_TIMEOUT_MS,
|
|
129
|
+
LOAD_TIMEOUT_BOUNDS,
|
|
130
|
+
),
|
|
131
|
+
show: checkBand(
|
|
132
|
+
'timeouts.show',
|
|
133
|
+
timeouts?.show ?? DEFAULT_SHOW_TIMEOUT_MS,
|
|
134
|
+
SHOW_TIMEOUT_BOUNDS,
|
|
135
|
+
),
|
|
136
|
+
adValidity: checkBand(
|
|
137
|
+
'adValidityMs',
|
|
138
|
+
adValidityMs ?? DEFAULT_AD_VALIDITY_MS,
|
|
139
|
+
AD_VALIDITY_BOUNDS,
|
|
140
|
+
),
|
|
141
|
+
appOpenValidity: checkBand(
|
|
142
|
+
'appOpenValidityMs',
|
|
143
|
+
appOpenValidityMs ?? DEFAULT_APP_OPEN_VALIDITY_MS,
|
|
144
|
+
APP_OPEN_VALIDITY_BOUNDS,
|
|
145
|
+
),
|
|
146
|
+
undecidedWarning: checkBand(
|
|
147
|
+
'undecidedWarningMs',
|
|
148
|
+
undecidedWarningMs ?? DEFAULT_UNDECIDED_WARNING_MS,
|
|
149
|
+
UNDECIDED_WARNING_BOUNDS,
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* How timers are scheduled, and the set that tracks them so `dispose()` can prove
|
|
156
|
+
* it released every one (SC-008): rn-core's, injectable so a test drives retry
|
|
157
|
+
* backoff, app-open expiry and the undecided warning by moving a clock.
|
|
158
|
+
*/
|
|
159
|
+
export { createTimerSet, type Scheduler, type TimerSet } from '@chipmobilesdk/rn-core';
|
|
160
|
+
|
|
161
|
+
/** A loaded ad past its validity window must never be reported ready (FR-034). */
|
|
162
|
+
export function isStale(loadedAtMs: number, nowMs: number, adValidityMs: number): boolean {
|
|
163
|
+
return nowMs - loadedAtMs >= adValidityMs;
|
|
164
|
+
}
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Correlation identifiers (FR-052).
|
|
3
|
-
*
|
|
4
|
-
* One value is minted when a load begins and carried by every event of that
|
|
5
|
-
* load/show cycle through its terminal event. It is what lets a consumer
|
|
6
|
-
* reconstruct a single ad's life from an event stream alone (SC-009) without the
|
|
7
|
-
* package having to store any history of its own.
|
|
8
|
-
*
|
|
9
|
-
* Opaque and monotonic per instance. Not a UUID, and deliberately not derived
|
|
10
|
-
* from anything about the device or the user — it exists to group events, and
|
|
11
|
-
* anything with more meaning than that would be a payload the package has
|
|
12
|
-
* promised not to carry (FR-021).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export interface CorrelationIssuer {
|
|
16
|
-
next(): string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function createCorrelationIssuer(prefix = 'ad'): CorrelationIssuer {
|
|
20
|
-
let counter = 0;
|
|
21
|
-
// Distinguishes cycles from two instances in one process, which otherwise
|
|
22
|
-
// both start at 1 and would collide in a merged log.
|
|
23
|
-
const instance = Math.random().toString(36).slice(2, 8);
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
next() {
|
|
27
|
-
counter += 1;
|
|
28
|
-
return `${prefix}-${instance}-${counter}`;
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Correlation identifiers (FR-052).
|
|
3
|
+
*
|
|
4
|
+
* One value is minted when a load begins and carried by every event of that
|
|
5
|
+
* load/show cycle through its terminal event. It is what lets a consumer
|
|
6
|
+
* reconstruct a single ad's life from an event stream alone (SC-009) without the
|
|
7
|
+
* package having to store any history of its own.
|
|
8
|
+
*
|
|
9
|
+
* Opaque and monotonic per instance. Not a UUID, and deliberately not derived
|
|
10
|
+
* from anything about the device or the user — it exists to group events, and
|
|
11
|
+
* anything with more meaning than that would be a payload the package has
|
|
12
|
+
* promised not to carry (FR-021).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface CorrelationIssuer {
|
|
16
|
+
next(): string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createCorrelationIssuer(prefix = 'ad'): CorrelationIssuer {
|
|
20
|
+
let counter = 0;
|
|
21
|
+
// Distinguishes cycles from two instances in one process, which otherwise
|
|
22
|
+
// both start at 1 and would collide in a merged log.
|
|
23
|
+
const instance = Math.random().toString(36).slice(2, 8);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
next() {
|
|
27
|
+
counter += 1;
|
|
28
|
+
return `${prefix}-${instance}-${counter}`;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|