@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/CHANGELOG.md
CHANGED
|
@@ -1,214 +1,224 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
- **
|
|
92
|
-
- **
|
|
93
|
-
|
|
94
|
-
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
- **
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
- **
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
- **
|
|
175
|
-
- **
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
- **
|
|
184
|
-
- **
|
|
185
|
-
- **
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
- **
|
|
198
|
-
- **
|
|
199
|
-
- **
|
|
200
|
-
- **
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
1
|
+
# @chipmobilesdk/rn-ads
|
|
2
|
+
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Packaging only; no code change. The package no longer declares `engines.node`: a
|
|
8
|
+
React Native package does not run on Node, so the field only produced install
|
|
9
|
+
warnings in apps on another Node version. rn-i18n's command-line tooling does run
|
|
10
|
+
on Node; its README's compatibility table states the version it needs. ([#14](https://github.com/operationd8979/chip-mobile-sdk/pull/14))
|
|
11
|
+
|
|
12
|
+
## [0.5.0] — 2026-11-01
|
|
13
|
+
|
|
14
|
+
Minor release: four defects on the production path. One exported union gains a member (`PrivacyUpdateResult` → `failed`), so an exhaustive `switch` over it needs one more case; nothing else requires a code change.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A throttled reload no longer strands a loaded full-screen ad.** With `minRequestIntervalMs` set on a full-screen placement, a second `load()` inside the window overwrote a `ready` (or `showing`) placement with `throttled`. `show()` then answered `not_ready` for an ad already paid for, and because `throttled` was not a state `load()` accepted, every later load failed `not_ready` until the instance was disabled. The throttle is now consulted after the `busy` and `not_ready` guards, a `ready` placement keeps its state when a reload is refused, and `throttled` accepts the next `load()` so the throttle alone decides when it proceeds. A load issued while an ad is on screen is answered `busy` without spending a throttle slot.
|
|
19
|
+
- **Withdrawing consent through `updatePrivacy()` stops pending retries.** Closing the gate this way now cancels every scheduled retry and halts in-flight loads, the same teardown a consent-flow outcome already performed. Retries also re-check the whole gate (availability, enablement, the app's gate and the consent flow) instead of enablement alone, so no retry reaches the provider without the permission the first request had.
|
|
20
|
+
- **`PrivacyOptions.personalizedAds: false` now requests non-personalized ads.** The AdMob adapter sent `requestNonPersonalizedAdsOnly: false` unconditionally and banners sent no request options at all. Every full-screen request and every banner now carries the current choice, including one changed through `updatePrivacy()`.
|
|
21
|
+
|
|
22
|
+
- **`updatePrivacy()` no longer reports `applied` when nothing was applied.** When the update opened the gate and triggered initialization, only `ready` and `blocked` were mapped; every other outcome fell through to `applied`. A failed initialization therefore looked like success, the app never retried, and no ad was ever shown with nothing to say why. The outcome is now mapped exhaustively.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- **`PrivacyUpdateResult` gains `{ status: 'failed', error: AdsError }`**, returned when the initialization an update triggered fails. `disabled`, `undecided` and `unavailable` from that initialization are passed through rather than reported as `applied`.
|
|
27
|
+
- **`updatePrivacy()` on a disposed instance returns `unavailable` / `provider_missing`**, as `initialize()` and `load()` already did. It returned `disabled`.
|
|
28
|
+
- `AdsEngine.createFullScreen` takes an optional third argument, `EngineRequestOptions` (`{ personalizedAds }`). Optional, so a custom engine written against 0.4.0 still satisfies the port; one that ignores it keeps requesting personalized ads. `FakeEngineCall` for `createFullScreen` records it as `request`.
|
|
29
|
+
|
|
30
|
+
## [0.4.0] — 2026-10-12
|
|
31
|
+
|
|
32
|
+
Additive minor release closing one gap that surfaced when `0.3.x` met an app that must let users change their consent choice. It is a **capability** gap, the same category as consent orchestration in `0.2.0`: the app could not close it without importing the provider, or without misusing `consent.reset()`, a testing-only call that wipes the stored decision while the form is open.
|
|
33
|
+
|
|
34
|
+
**Upgrading requires no code change.** No existing exported union gained a member. Three behaviour changes are worth reading — see [Changed](#changed) — and one defect is fixed — see [Fixed](#fixed).
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **Privacy options: the "change my choice" form** (`ads.consent.showPrivacyOptionsForm()`). Presents the provider's privacy options form and returns a typed `PrivacyOptionsFormResult`, never throwing:
|
|
39
|
+
- `shown`, carrying the `ConsentSnapshot` the form left behind, already applied to the gate. A user who backs out unchanged lands here with the state they had.
|
|
40
|
+
- `not_required`, `info_update_required`, `busy`: refusals that present nothing.
|
|
41
|
+
- `failed`, carrying an `AdsError` (`consent_flow_failed`).
|
|
42
|
+
- `unavailable`: `unsupported_platform` on iOS, `provider_missing` after `dispose()`.
|
|
43
|
+
|
|
44
|
+
The outcome feeds the privacy gate by itself, exactly like `showFormIfRequired`, and combines with the app's own gate as most-restrictive-wins. The package still draws no UI. Where the entry lives, its copy and when to offer it stay the app's.
|
|
45
|
+
|
|
46
|
+
- **`ConsentSnapshot.privacyOptionsRequirement`** (`'required' | 'not_required' | 'unknown'`): whether to show that entry at all. Refreshed by `requestInfoUpdate()` and published through `consent.subscribe`. It is the provider's verdict forwarded, never a region judgement of the package's. It is `unknown` before the first successful update, after `reset()`, after a failed update, and on iOS.
|
|
47
|
+
|
|
48
|
+
- **A failed form is never consent.** When the form fails, the package asks the provider what it still holds rather than assuming: the form never completed, so the stored decision has not changed, and revoking it would punish a user for a form that failed to open. If the provider cannot answer either, the state becomes `error` and the gate closes.
|
|
49
|
+
|
|
50
|
+
- **`./testing`**: `setPrivacyOptionsFormResult`, `failPrivacyOptionsForm`, `holdPrivacyOptionsForm` (keeps the next form open until the test closes or fails it, so `busy`, a consent update or `dispose()` can happen while it is on screen), `failConsentInfoRead`, and a `privacyOptions` field on the scripted consent info. `FakeConsentCall` gained `{ kind: 'showPrivacyOptionsForm' }`.
|
|
51
|
+
|
|
52
|
+
- Exported types `PrivacyOptionsFormResult` and `PrivacyOptionsRequirement`.
|
|
53
|
+
|
|
54
|
+
No new event type. Consent changes caused by the form arrive through `consent.subscribe`, as `initialization` events when the status moves, and as `unavailable` for a load the withdrawal cancels. No payload carries the TCF string or any identifier.
|
|
55
|
+
|
|
56
|
+
No bridge change. The pinned `react-native-google-mobile-ads` `16.0.3` already exposes `AdsConsent.showPrivacyOptionsForm()` and `privacyOptionsRequirementStatus`, verified against the installed type declarations and its Android module. The range stays `>=16.0.3 <16.1.0`, and the merged manifest gains no permission.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **`initialize()` waits for the package's consent flow, once the app uses it.** After any `ads.consent.*` call, `initialize()` on a flow that does not permit requests — declined, not yet answered, or failed — returns `{ status: 'blocked', reason: 'consent_flow_required' | 'consent_flow_error' }` and initializes nothing natively. `0.3.0` checked only the app's own gate here, so it returned `ready` and initialized the provider for a user who had just declined, while `load` refused; that contradicts the provider's guidance not to initialize before consent permits requests. When the flow later permits requests, through either consent form, the package initializes the provider itself, once. An app that runs update → form → `initialize()` sees no difference for a user who consents. An app that never calls `ads.consent.*` is unaffected.
|
|
61
|
+
|
|
62
|
+
- **A consent gate that closes while the instance is `ready` now moves it to `blocked`, and stops what is in flight.** `0.3.0` kept `ready` while `load` already refused. That was harmless while the only consent outcome that could close the gate ran before anything loaded. The privacy options form changes that: a user can withdraw with a banner mounted, and a banner host keyed on `ready` would have gone on requesting. Now in-flight loads settle with `{ status: 'blocked', reason }` instead of being left waiting, loaded-but-unshown ads are released, pending retries are cancelled, and a mounted `AdsBanner` leaves the tree. An ad already on screen is still not force-closed. This also applies when a later `requestInfoUpdate()` fails.
|
|
63
|
+
|
|
64
|
+
- **Reopening a gate on an already-initialized instance resumes without a second native initialization.** `0.3.0` initialized the provider again on `ready → blocked → reopened`, through the consent flow or through `updatePrivacy`, against the one-initialization-per-instance rule. Now the status returns to `ready` with no native call, including when the app re-runs `initialize()` after a consent change. Resuming goes through the whole gate, so re-running `initialize()` cannot reopen a consent the user just withdrew. One exception keeps the old path: if an init-time flag (`childDirected`, `underAgeOfConsent`, `maxAdContentRating`) changed in between, a real initialization still runs, because that is the only way the flag can take effect.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- **A `load()` in flight when ads are turned off, turned `undecided`, or the instance is disposed now settles.** It resolves with `{ status: 'disabled' }`, `{ status: 'undecided' }`, or `{ status: 'unavailable', reason: 'provider_missing' }` — the answer a `load` started at that moment gets. Before `0.4.0` the resources were released but the promise was never resolved, so an app awaiting a load across an ad-removal purchase waited forever. Disabling and undeciding also emit the matching refusal event for the abandoned load; dispose emits nothing, as always.
|
|
69
|
+
- **A handle the provider returned after such a disable is disposed instead of adopted.** Previously it was subscribed and went on loading behind a disabled instance.
|
|
70
|
+
|
|
71
|
+
### Not added, deliberately
|
|
72
|
+
|
|
73
|
+
- **No Settings entry, copy or offer timing.** Those are policy, and they stay the app's.
|
|
74
|
+
- **No region detection of the package's own.** The requirement is the provider's answer.
|
|
75
|
+
- **iOS** stays `unavailable` / `unsupported_platform`.
|
|
76
|
+
|
|
77
|
+
## [0.3.0] — 2026-10-10
|
|
78
|
+
|
|
79
|
+
Additive minor release closing one gap that surfaced when `0.2.x` met an app placing a banner inside a view the user swipes between. It is a **capability** gap: the app could not close it itself without reaching past the package, which is the thing this package exists to prevent.
|
|
80
|
+
|
|
81
|
+
**Upgrading requires one change, compile-time only**, plus one behaviour change worth reading: see [Changed](#changed-1).
|
|
82
|
+
|
|
83
|
+
### Added
|
|
84
|
+
|
|
85
|
+
- **Per-placement request throttling** (`minRequestIntervalMs`). A minimum interval between provider *requests* for one placement.
|
|
86
|
+
|
|
87
|
+
The pattern it exists for: a banner mounted inside a view the user navigates between — a day in a calendar, a tab, a row scrolling in and out of a list — is unmounted and remounted by React on every navigation, and **each remount is a billable request**. Ten quick swipes were ten requests in a few seconds. Ad networks read that as invalid traffic, and the penalty is account-level and not recoverable by shipping a fix.
|
|
88
|
+
|
|
89
|
+
That is why this belongs in the package rather than in a README note. It sits in the same class as the test-ad substitution already enforced in non-production builds: a safety property the library guarantees rather than documents and hopes each app remembers.
|
|
90
|
+
|
|
91
|
+
- **Format-aware defaults.** `60 000 ms` for a banner — AdMob's own documented floor for banner auto-refresh, so the number is the provider's rather than invented. `0` (off) for `interstitial`, `rewarded` and `app_open`: those are loaded by an explicit call, not mounted by a renderer, so they never had the problem, and a default floor would break an app that legitimately loads three times in two minutes.
|
|
92
|
+
- **Per-placement override**, bounds `0`–`600 000 ms`, validated eagerly at `createAds` and naming the offending placement — so a misconfiguration fails at construction rather than by refusing every request in production, which is the one moment nobody is watching for a configuration error.
|
|
93
|
+
- **Rotation is exempt.** A width re-adaptation inside one mount is the same banner the placement already requested; refusing it would leave a rotated screen with no banner for up to a minute. The slot is charged once per *mount*, not per request.
|
|
94
|
+
- **A concurrent duplicate load is exempt.** Those callers already join the in-flight request, and charging the second would turn an existing guarantee into a refusal.
|
|
95
|
+
- **Clock-robust.** A backwards clock — a manual time change, an NTP correction — fails open rather than locking a placement out until the clock catches up. The throttle guards against bursts, and a backwards clock is not a burst.
|
|
96
|
+
|
|
97
|
+
- `PlacementState` gains `'throttled'`, and `LoadResult` gains `{ status: 'throttled', retryAfterMs }`. Deliberately distinct from `'failed'` and from a no-fill: no-fill means the network had nothing to give, this means we did not ask. An app that conflates them will misdiagnose the first time a placement goes quiet.
|
|
98
|
+
|
|
99
|
+
- `request_throttled` event, carrying `retryAfterMs` and the `minRequestIntervalMs` that refused it, so the cause is readable from the event stream alone. No `load_started` accompanies it — a refused request is not a cycle, and the stream must not claim a billable request that never happened.
|
|
100
|
+
|
|
101
|
+
- Exported `DEFAULT_BANNER_MIN_REQUEST_INTERVAL_MS`, `DEFAULT_FULL_SCREEN_MIN_REQUEST_INTERVAL_MS`, `MIN_REQUEST_INTERVAL_BOUNDS`, `defaultMinRequestIntervalMs`, so an app reads the floor it is bound by instead of discovering it from a `throttled` state in production.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- **A banner placement now has a 60s floor between requests.** If your banner lives inside a list row, a tab, or a view the user swipes between, it will report `'throttled'` after a quick navigation where `0.2.x` silently issued another request. That silent request was the defect; this is the fix.
|
|
106
|
+
|
|
107
|
+
Before reaching for `minRequestIntervalMs: 0`, consider the architectural answer: a banner kept **outside** the subtree that remounts stays mounted across navigation, keeps its ad, and costs one request. The throttle is a safety net for when that is not possible.
|
|
108
|
+
|
|
109
|
+
- Three unions gained a member, so an exhaustive `switch` over any of them needs the new case or a `default`:
|
|
110
|
+
|
|
111
|
+
| Union | New member |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `PlacementState` | `'throttled'` |
|
|
114
|
+
| `LoadResult` | `{ status: 'throttled', retryAfterMs }` |
|
|
115
|
+
| `AdEventType` | `'request_throttled'` |
|
|
116
|
+
|
|
117
|
+
- Full-screen formats are unchanged: their default floor is `0`, so request counts for `interstitial`, `rewarded` and `app_open` are byte-identical to `0.2.x` unless you configure a floor.
|
|
118
|
+
|
|
119
|
+
### Not added, deliberately
|
|
120
|
+
|
|
121
|
+
- **No frequency cap, and none planned.** This release limits how often a placement may *ask*; how often an app *shows* remains entirely the app's. Pacing has no correct default — one app counts items created, another counts screens opened, another counts minutes — so shipping one would make the package own policy that belongs to the app. For "an interstitial every third save", count the saves in the app and call `show` when the rule says so.
|
|
122
|
+
|
|
123
|
+
## [0.2.1] — 2026-09-17
|
|
124
|
+
|
|
125
|
+
### Changed
|
|
126
|
+
|
|
127
|
+
- The shipped agent skill was rewritten: a clearer description and more task
|
|
128
|
+
routing. No code change.
|
|
129
|
+
|
|
130
|
+
## [0.2.0] — 2026-09-13
|
|
131
|
+
|
|
132
|
+
Additive minor release closing five gaps that surfaced when `0.1.0` met an app selling an ad-removal tier. Four of the five were gaps in **capability** — an app could not patch them itself without importing the provider directly, which is the one thing this package exists to prevent.
|
|
133
|
+
|
|
134
|
+
**Upgrading requires one change, and it is compile-time only**: see [Changed](#changed-2) and the migration guide in the README.
|
|
135
|
+
|
|
136
|
+
### Added
|
|
137
|
+
|
|
138
|
+
- **Consent orchestration** (`ads.consent.*`), optional. Runs the provider's complete consent flow — information update, form presentation, outcome — with your app importing nothing of the provider's. The outcome feeds the existing privacy gate by itself, in the order the gate requires, without producing a second native initialization. The package renders no consent UI; the form is the provider's.
|
|
139
|
+
- Normalized, provider-neutral `ConsentState`: `undetermined`, `not_required`, `required`, `obtained`, `error`. No provider type, no raw payload, no encoded consent string.
|
|
140
|
+
- **A failure is never consent.** A provider error, a network failure, a cancellation, and a form dismissed without an answer all produce `error`, and `error` never carries permission.
|
|
141
|
+
- Combines with an app-supplied gate as **most-restrictive-wins** — either input alone can block, and the reported reason names which one is blocking.
|
|
142
|
+
- **Consent testing tools**: `consentTesting.debugGeography` forces a region, `consentTesting.resetOnStart` clears the stored decision. **Both are stripped in `production`**, at configuration validation rather than at each call site, and the fact that they were ignored is observable through a `config_warning` and the new `getIgnoredConsentTools()`.
|
|
143
|
+
- **Three-state enablement.** `enabled` now accepts `'undecided'` alongside `true` and `false`, for an app that cannot answer at construction time. Sends zero requests exactly like `disabled`, but reports itself distinctly so a banner host can hold its space instead of reclaiming it. The package never resolves it for you; leaving it unresolved past `undecidedWarningMs` emits one `config_warning`, and only one.
|
|
144
|
+
- **`setEnablement(next)`** for the three-state value, alongside `setEnabled(boolean)`.
|
|
145
|
+
- **App-open format** (`format: 'app_open'`), sharing the same single full-screen lock as interstitial and rewarded, with its own 30-minute validity window (`appOpenValidityMs`, 1 minute – 1 hour) and its own official test ad unit. The package never shows one on its own.
|
|
146
|
+
- **Ad-originated return refusal.** A show requested on a return from a full-screen ad — or from a browser an ad opened — is refused with `ad_originated_return`, distinct from `busy` and `not_ready`. Only the package can know this, which is why the rule lives here rather than in your app. It cannot survive process death; see the README for the boundaries.
|
|
147
|
+
- **Bounded retry**, **off by default**, configurable instance-wide via `retry` and per placement via `PlacementDefinition.retry` (`false` disables one placement). Two retries at 2s then 4s by default, exponential with a fixed x2 multiplier and no jitter. Only `retryable` errors are retried. Disabling ads, moving to undecided, losing consent, or `dispose()` cancels every pending retry across every placement. Every attempt emits a `retry_scheduled` event carrying the load cycle's `correlationId`, so a bill can be reconciled against the event stream.
|
|
148
|
+
- **Defined precedence for the three blocking gates** — platform availability, then enablement, then consent — documented as a full lookup table in the README and enforced by a single function rather than by four agreeing `if` chains.
|
|
149
|
+
- New exported constants: `DEFAULT_APP_OPEN_VALIDITY_MS`, `APP_OPEN_VALIDITY_BOUNDS`, `DEFAULT_RETRY_POLICY`, `RETRY_BOUNDS`, `DEFAULT_UNDECIDED_WARNING_MS`, `UNDECIDED_WARNING_BOUNDS`.
|
|
150
|
+
- `./testing` gained consent control (`setConsentInfo`, `failConsentRequest`, `failConsentForm`, `consentCalls`), app-open support, and `createTestClock` — so retry timing, app-open expiry, the undecided warning, and the ad-originated windows are all testable with no device and no waiting.
|
|
151
|
+
|
|
152
|
+
### Changed
|
|
153
|
+
|
|
154
|
+
- **`setEnabled` returns `EnablementResult` instead of `void`.** Existing call sites that ignore the return value still compile — a value-returning function is assignable to a `void`-returning signature.
|
|
155
|
+
- **Five public unions gained members**, which breaks exhaustive `switch` statements in consumer TypeScript. This is the single required migration step, it is compile-time only, and it has no runtime equivalent:
|
|
156
|
+
|
|
157
|
+
| Union | New members |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `AdFormat` | `'app_open'` |
|
|
160
|
+
| `InitStatus` | `'undecided'` |
|
|
161
|
+
| `PlacementState` | `'undecided'` |
|
|
162
|
+
| `PrivacyBlockReason` | `'consent_flow_required'`, `'consent_flow_error'` |
|
|
163
|
+
| `AdsErrorCode` | `'undecided'`, `'ad_originated_return'`, `'consent_flow_required'`, `'consent_flow_failed'`, `'retry_out_of_bounds'` |
|
|
164
|
+
|
|
165
|
+
- **Behavior change — gate reason reported when two gates are closed at once.** On an unavailable platform with ads also disabled, the reported reason is now `unsupported_platform` where `0.1.0` reported `disabled`. In `0.1.0` the surviving state depended on which condition arrived last, so the outcome was not deterministic; availability now wins consistently. **Request counts are unchanged** and no action is required. This is listed here rather than among the additions because it is a real, if narrow, change in observable output.
|
|
166
|
+
|
|
167
|
+
### Fixed
|
|
168
|
+
|
|
169
|
+
- **Re-enabling ads no longer fails in silence.** In `0.1.0`, `setEnabled(true)` after a disable dropped the init status to `not_started`, so every subsequent `load` and `show` was refused, and the `void` return gave the caller no signal. It now resumes to the state held before disabling — with zero provider calls, because disabling never un-initialized the provider — or returns `requires_initialization` explicitly. The package does not initialize implicitly on your behalf.
|
|
170
|
+
- **`UNAVAILABLE` and `DISABLE` had no mutual guard** in the initialization machine, so the surviving state depended on arrival order. Availability now dominates deterministically.
|
|
171
|
+
|
|
172
|
+
### Compatibility
|
|
173
|
+
|
|
174
|
+
- **No dependency change.** `peerDependencies` is untouched: the pinned `react-native-google-mobile-ads@>=16.0.3 <16.1.0` already exposes everything the consent flow and app-open need, verified against the installed type declarations. The Kotlin toolchain constraint documented in `0.1.0` is not reopened.
|
|
175
|
+
- **With no new configuration, runtime behavior is identical to `0.1.0`, request counts included.** This is verified by `src/__tests__/compat-0-1-0.test.ts`, which asserts an identical engine call sequence — in count and order — rather than leaving it as a promise in this file.
|
|
176
|
+
- The complete `0.1.0` test suite passes against this release, with two mechanical exceptions that could not survive by construction: the test pinning the version string, and a test that used `app_open` as its example of an *unsupported* format.
|
|
177
|
+
|
|
178
|
+
### Deliberately still not included
|
|
179
|
+
|
|
180
|
+
- **Frequency caps, placement rules, interruption timing, and user segmentation.** Still app policy. Still a decision, not an omission.
|
|
181
|
+
- **Any knowledge of payments, entitlements, or premium tiers.** The `'undecided'` state exists *because* consuming apps have such a model, but the package receives a decision and never a reason.
|
|
182
|
+
- **Self-rendered consent UI, legal copy, or jurisdiction classification.** The package orchestrates; the provider renders; the publisher decides.
|
|
183
|
+
- **Third-party consent platforms** other than the one shipping with the ad provider.
|
|
184
|
+
- **Native ads, rewarded-interstitial, and mediation.**
|
|
185
|
+
- **iOS.** Every surface added here was shaped so an iOS adapter stays purely additive — in particular, `ConsentState` can express both a consent flow and App Tracking Transparency, and their ordering, without changing shape.
|
|
186
|
+
|
|
187
|
+
## [0.1.0] — 2026-09-02
|
|
188
|
+
|
|
189
|
+
Initial release. No migration required.
|
|
190
|
+
|
|
191
|
+
### Added
|
|
192
|
+
|
|
193
|
+
- **Provider-neutral public contract** for configuration, initialization, availability, load, show, events, errors, and cleanup. Consuming apps never import the Google Mobile Ads SDK; they name placement keys they chose.
|
|
194
|
+
- **Three ad formats on Android**: anchored adaptive banner (`AdsBanner`), interstitial, and rewarded.
|
|
195
|
+
- **Production-traffic guarantee.** In `development` and `test`, any ad unit that is not one of Google's official test units is replaced with one, reported as a `config_warning` naming every affected placement and surfaced through `getSubstitutedPlacements()`. `production` never substitutes. A debug build declaring `production` fails closed with `production_in_dev_build`.
|
|
196
|
+
- **Enforced consent ordering.** No provider call of any kind occurs before the app's privacy gate permits requests. Child-directed, under-age-of-consent, content-rating, and personalization options are applied before the provider initializes; changing one afterwards reports `requires_reinitialization` rather than half-applying.
|
|
197
|
+
- **Idempotent initialization.** Concurrent callers share one operation; the native SDK initializes at most once.
|
|
198
|
+
- **Single full-screen lock.** At most one interstitial or rewarded ad is ever displayed. A refused show returns `busy` immediately and is never queued.
|
|
199
|
+
- **Trustworthy rewards.** `reward_earned` is emitted only on provider confirmation, exactly once, and never inferred from dismissal. Both callback orderings produce the same outcome.
|
|
200
|
+
- **Normalized event and error model** with a per-cycle `correlationId`, stable error codes, a `retryable` flag, and redacted causes. No provider object, advertising identifier, or consent payload crosses the boundary.
|
|
201
|
+
- **Graceful degradation.** Ads disabled, an unsupported platform, a missing bridge, or a failing provider are ordinary conditions — no public operation throws.
|
|
202
|
+
- **Bounded timeouts and staleness.** Load 30s, show 10s, loaded-ad validity 60 minutes, each configurable within a documented band. Out-of-band values are a configuration error rather than silently clamped.
|
|
203
|
+
- **`./testing` entrypoint** exporting `createFakeAdsEngine`, so consumers can test their own integration with no device, network, or ad inventory.
|
|
204
|
+
|
|
205
|
+
### Deliberately not included
|
|
206
|
+
|
|
207
|
+
- **Automatic retry.** Retry pacing belongs to the app; every retry is a billable request. The contract is shaped so bounded, disableable retry can be added later without a breaking change.
|
|
208
|
+
- **A consent management platform.** The package consumes a gate outcome and enforces ordering around it. Consent UX, copy, and policy are the app's.
|
|
209
|
+
- **Frequency capping or placement rules.** The package never chooses when to interrupt a user.
|
|
210
|
+
- **Any analytics dependency.** Events are emitted; forwarding them is the app's choice.
|
|
211
|
+
|
|
212
|
+
### Known limitations
|
|
213
|
+
|
|
214
|
+
- iOS is not implemented; the package reports `unavailable` / `unsupported_platform` and does not break an iOS build.
|
|
215
|
+
- App-open, native, and rewarded-interstitial formats, and cross-network mediation, are deferred.
|
|
216
|
+
- `impression` for full-screen formats is derived from the ad opening rather than provider-reported, because the provider's full-screen event enum has no impression member. Banners carry a true provider impression. The distinction is exposed as `source: 'provider' | 'derived'`.
|
|
217
|
+
|
|
218
|
+
### Compatibility notes
|
|
219
|
+
|
|
220
|
+
- `react-native-google-mobile-ads` is pinned to **`>=16.0.3 <16.1.0`**, not an open caret. Bridge 16.1.0+ bundles Google Mobile Ads 25.x, which carries Kotlin 2.3.0 metadata that React Native 0.85's pinned Kotlin 2.1.x compiler cannot read. The closed upper bound makes that a resolution failure at install time rather than a build failure later. Revisit when React Native ships a Kotlin 2.3 compiler.
|
|
221
|
+
- Consuming apps must declare `react-native-google-mobile-ads.android_app_id` in their own `app.json`. This package carries no application ID and no ad unit ID.
|
|
222
|
+
- The provider's manifest merges `INTERNET`, `ACCESS_NETWORK_STATE`, and `WAKE_LOCK`; `com.google.android.gms.permission.AD_ID` arrives transitively from `play-services-ads`. Apps that must not access the Advertising ID remove it with `tools:node="remove"`.
|
|
223
|
+
|
|
224
|
+
[0.1.0]: https://github.com/chipmobilesdk/ChipMobileSdk/releases/tag/rn-ads%400.1.0
|