@chipmobilesdk/rn-ads 0.4.0 → 0.5.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.
@@ -1,274 +1,274 @@
1
- ---
2
- name: sdk-rn-ads
3
- description: "Advertising via @chipmobilesdk/rn-ads: AdMob banner/interstitial/rewarded/app-open behind a provider-neutral contract, placement keys, optional consent orchestration incl. the privacy options (change my consent) form, three-state enablement, bounded retry, per-environment test-ad substitution, per-placement request throttling. Use when adding ads to a screen, running a consent flow without importing AdMob, adding a Privacy options entry to Settings, deferring ads until a purchase check resolves, debugging an ad that never loads or reports throttled, placing a banner inside a list or a view the user swipes between, mapping ad units per environment, or answering a Data safety question about advertising."
4
- metadata:
5
- package: "@chipmobilesdk/rn-ads"
6
- packageVersion: "0.4.0"
7
- user-invocable: true
8
- disable-model-invocation: false
9
- ---
10
-
11
- # @chipmobilesdk/rn-ads
12
-
13
- Adaptive banner, interstitial, rewarded and app-open ads on Android via Google AdMob,
14
- behind a contract that names no provider. The app names a placement key; the package owns
15
- the request. Consuming code never imports the Google Mobile Ads SDK — including for
16
- consent.
17
-
18
- ## Use this when
19
-
20
- - Adding a banner, interstitial, rewarded or app-open ad to a screen
21
- - Running a consent flow in a region that requires one, without importing AdMob
22
- - Letting a user change their consent later (a "Privacy options" entry in Settings)
23
- - Deferring ads at startup until a purchase or entitlement check answers
24
- - Turning ads back on after a subscription lapses or a refund lands
25
- - Mapping AdMob ad units to placement keys per environment
26
- - Debugging an ad that never loads, or a `blocked` / `undecided` / `unavailable` /
27
- `ad_originated_return` / `provider_error`
28
- - Answering a Data safety / App Privacy question about advertising
29
-
30
- ## Do not use this for
31
-
32
- - **Consent UI or copy** — the package *orchestrates* the provider's flow and normalizes
33
- the outcome, but the form is drawn by the provider and the legal text is theirs. Timing,
34
- surrounding copy, and where the "change your mind" entry lives stay yours.
35
- - **Where or how often ads appear** — no frequency cap, no placement rule, no auto-insert.
36
- The package never shows an ad on its own, app-open included.
37
- - **Granting the reward** — it reports `reward_earned`; entitlement is the app's.
38
- - **Anything about payments** — the undecided state exists *because* apps sell ad removal,
39
- but the package receives a decision and never a reason, receipt or entitlement.
40
- - iOS — not implemented; every call returns `unavailable` / `unsupported_platform`.
41
-
42
- ## Nothing loads until consent and initialization complete
43
-
44
- This is enforced, and it is the answer to almost every "the ad never appears". With the
45
- gate withheld, **zero** provider calls occur — not even configuration. `childDirected`,
46
- `underAgeOfConsent` and `maxAdContentRating` take effect only if they reach
47
- `initialize()` *before* the provider starts; changing one later returns
48
- `requires_reinitialization` rather than silently not applying.
49
- Once you call `ads.consent.*`, the flow is part of that gate: `initialize()` returns
50
- `blocked` · `consent_flow_required` until it permits, and the package initializes by
51
- itself when it does — do not retry `initialize()` in a loop.
52
- See `## Initialize — consent comes first` and `## Consent`.
53
-
54
- ## "Change my consent" is covered — never `reset()` + re-show
55
-
56
- New in 0.4.0. Show your Settings entry when `consent.getState().privacyOptionsRequirement`
57
- is `'required'` (hide it on `'unknown'` and `'not_required'`), and call
58
- `ads.consent.showPrivacyOptionsForm()` from it. Where the entry lives, and its copy, stay
59
- yours. The result feeds the gate by itself. A withdrawal moves `ready` → `blocked` and
60
- cancels in-flight loads; a re-grant resumes with **no second native initialization**.
61
- `consent.reset()` is testing-only — it wipes the stored decision while the form is open.
62
- See `### Letting users change their choice` under `## Consent`.
63
-
64
- ## Three gates, one reason — check this order first
65
-
66
- A refused request reports exactly one reason, and the precedence is fixed:
67
- **platform availability → enablement → consent**, most immovable first. So
68
- `unsupported_platform` on a disabled instance is reported as `unsupported_platform` —
69
- turning ads on would not have helped. Full table in `## Why an ad did not show`.
70
-
71
- ## Never gate `<AdsBanner>` on its own placement state
72
-
73
- `AdsBanner` is what measures the container and starts the load, so it must be mounted
74
- unconditionally. Rendering it only when the state is `loading`/`ready` deadlocks at
75
- `idle` and no ad ever appears. Use the state for the *surrounding* placeholder and to
76
- decide whether to hold or reclaim space — never to decide whether to mount the host. It
77
- renders `null` on its own when there is nothing to show.
78
-
79
- ## `undecided` is not `disabled`, and that is the whole point
80
-
81
- `enabled` takes `true`, `false` or `'undecided'`. All three send zero requests except
82
- `true`. `'undecided'` reports itself distinctly — in `getStatus()` and in the banner's
83
- placement state — so an app that has not yet heard back from the store can **hold** the
84
- banner's space instead of reclaiming it, and a paying user never sees an ad flash in and
85
- out. `disabled` means a settled no; reclaim the space.
86
-
87
- The package never resolves it for you. Leave it unresolved past `undecidedWarningMs`
88
- (default 10s) and you get exactly one `config_warning`, never a stream.
89
-
90
- ## `setEnabled` returns a result — read it
91
-
92
- In 0.1.0 it returned `void` and re-enabling after a disable silently dropped the init
93
- status, so every later `load` and `show` was refused with nothing to say so. That was a
94
- defect and it is fixed. The result now says which happened:
95
-
96
- - `resumed` — the provider was never un-initialized; load away, zero provider calls made.
97
- - `requires_initialization` — call `initialize()` yourself. The package will **not** do it
98
- implicitly, because that would hide an async, billable operation behind a setter.
99
-
100
- Existing `ads.setEnabled(false);` call sites still compile untouched.
101
-
102
- ## A banner in a list or a swipeable view gets throttled — by design
103
-
104
- New in 0.3.0. A banner mounted inside a view the user navigates between — a day in a
105
- calendar, a tab, a row that scrolls in and out — is unmounted and remounted by React on
106
- every navigation, and **each remount is a billable request**. Ten quick swipes used to be
107
- ten requests in a few seconds, which ad networks read as invalid traffic. The penalty is
108
- account-level and no shipped fix recovers it.
109
-
110
- So each placement now has a minimum interval between *requests*: **60s for a banner**
111
- (AdMob's own refresh floor), **off for full-screen formats** — those are loaded by an
112
- explicit call, not mounted by a renderer, so they never had the problem. A refusal reports
113
- `throttled` on the placement and emits `request_throttled` with `retryAfterMs`.
114
-
115
- Three things to know before you reach for `minRequestIntervalMs: 0`:
116
-
117
- - **`throttled` is not `failed` and not a no-fill.** Nothing is wrong and nothing was
118
- requested. Collapse the space if you like, but do not log it as an error.
119
- - **Rotation is exempt.** A width re-adaptation inside one mount is not charged, so a
120
- rotated screen keeps its banner.
121
- - **The better fix is architectural.** Keep the banner outside the subtree that remounts
122
- and the throttle never fires. It is a safety net, not a licence to ignore the problem.
123
-
124
- This is **not** a frequency cap. It limits how often a placement may *ask*; how often you
125
- *show* is yours, and the package deliberately ships no pacing (see `## Do not use this for`).
126
-
127
- ## Five things not to "fix"
128
-
129
- - **The bridge pin.** `react-native-google-mobile-ads` is peered at `>=16.0.3 <16.1.0`;
130
- 16.1.0+ carries Kotlin 2.3 metadata that React Native 0.85's pinned compiler cannot
131
- read, and the Android build fails outright. `0.2.0` and `0.4.0` needed no bump —
132
- consent, app-open and privacy options are all already in `16.0.3`. Read
133
- `## Compatibility` before widening it.
134
- - **Test ads in dev.** Outside `production`, non-official ad units are replaced with
135
- Google's test units and reported via `config_warning` / `getSubstitutedPlacements()`.
136
- That is the safety guarantee working, not a misconfiguration. Never click a production
137
- ad to verify anything.
138
- - **Consent tools going quiet in production.** `consentTesting.debugGeography` and
139
- `resetOnStart` are stripped at config validation in `production` and listed by
140
- `getIgnoredConsentTools()`. Working as designed.
141
- - **A declined consent still reporting `obtained` / `canRequestAds: true`.** That is UMP's
142
- meaning, not a bug: `OBTAINED` means *an answer was recorded*, and non-personalized ads
143
- stay servable. The package forwards the provider's verdict rather than inventing one.
144
- - **A banner reporting `throttled` right after a navigation.** That is 0.3.0's request
145
- floor working — see the section above. Lower it knowingly or move the banner out of the
146
- remounting subtree; do not set `0` to make the symptom go away.
147
-
148
- ## Retry is off unless you ask
149
-
150
- No `retry` in config means the request count is byte-identical to 0.1.0 — a test in the
151
- package proves it. When enabled it is bounded (default 2 retries, 2s then 4s, no jitter),
152
- retries only errors already flagged `retryable`, and cancels everything on disable,
153
- undecided, consent loss or `dispose()`. Every attempt emits `retry_scheduled` carrying the
154
- load cycle's `correlationId`, because every attempt is a billable request.
155
- Per-placement `retry: false` turns it off for one placement.
156
-
157
- ## App-open: the one rule you cannot implement yourself
158
-
159
- App-open shares the single full-screen lock with interstitial and rewarded, and expires on
160
- its own shorter window (30 min default, vs 60). The part worth knowing: a show requested
161
- on a return **from an ad** — or from a browser an ad opened — is refused with
162
- `ad_originated_return`, distinct from `busy` and `not_ready`. Your app cannot tell that
163
- return apart from any other; the package can. It cannot survive process death, and a
164
- browser opened without a preceding click is undetectable. See `## App open`.
165
-
166
- ## Public API (v0.4.0)
167
-
168
- Runtime: `createAds` (throws `AdsConfigError` on a bad catalog or an out-of-range retry
169
- policy; everything else returns a typed result rather than throwing) · `AdsProvider`,
170
- `useAds` · `AdsBanner` · `usePlacementState`, `useAdsStatus`, `useAdsEvents` · `AdsError`,
171
- `AdsConfigError`, `isAdsError`, `isFullScreenFormat` · `DEFAULT_LOAD_TIMEOUT_MS`,
172
- `DEFAULT_SHOW_TIMEOUT_MS`, `DEFAULT_AD_VALIDITY_MS`, `DEFAULT_APP_OPEN_VALIDITY_MS`,
173
- `DEFAULT_RETRY_POLICY`, `DEFAULT_UNDECIDED_WARNING_MS`, `LOAD_TIMEOUT_BOUNDS`,
174
- `SHOW_TIMEOUT_BOUNDS`, `AD_VALIDITY_BOUNDS`, `APP_OPEN_VALIDITY_BOUNDS`, `RETRY_BOUNDS`,
175
- `UNDECIDED_WARNING_BOUNDS`, `DEFAULT_BANNER_MIN_REQUEST_INTERVAL_MS`,
176
- `DEFAULT_FULL_SCREEN_MIN_REQUEST_INTERVAL_MS`, `MIN_REQUEST_INTERVAL_BOUNDS`,
177
- `defaultMinRequestIntervalMs` · `createFakeAdsEngine`, `createTestClock` from
178
- `@chipmobilesdk/rn-ads/testing` (test doubles only).
179
-
180
- Instance: `initialize`, `updatePrivacy`, `setEnabled`, `setEnablement`, `setAppActive`,
181
- `consent`, `getStatus`, `subscribeStatus`, `load`, `show`, `getPlacementState`,
182
- `subscribePlacement`, `subscribeEvents`, `getSubstitutedPlacements`,
183
- `getIgnoredConsentTools`, `dispose`.
184
-
185
- Consent namespace (`ads.consent.*`, opt-in — untouched, nothing starts):
186
- `requestInfoUpdate`, `showFormIfRequired`, `showPrivacyOptionsForm`, `getState`,
187
- `subscribe`, `reset`.
188
-
189
- Types: `AdsInstance`, `AdsConfig`, `PlacementDefinition`, `PrivacyOptions`, `AdFormat`,
190
- `FullScreenFormat`, `Environment`, `Enablement`, `EnablementResult`, `ConsentApi`,
191
- `ConsentState`, `ConsentSnapshot`, `ConsentTestingOptions`, `PrivacyOptionsRequirement`,
192
- `PrivacyOptionsFormResult`, `RetryPolicy`, `InitStatus`,
193
- `PlacementState`, `InitResult`, `LoadResult`, `ShowResult`, `PrivacyUpdateResult`,
194
- `Reward`, `AdEvent`, `AdEventType`, `AdEventListener`, `ConfigWarning`, `ImpressionSource`,
195
- `AdsErrorCode`, `AdsOperation`, `TimeoutConfig`, `AdContentRating`, `PrivacyBlockReason`,
196
- `UnavailableReason`, `Unsubscribe`, `AdsBannerProps`, `AdsProviderProps`.
197
-
198
- Call `dispose()` when the owning screen unmounts; it is idempotent, and so is every
199
- `Unsubscribe`.
200
-
201
- ## Upgrading 0.3.x → 0.4.0
202
-
203
- No code change: no existing union gained a member. New: `showPrivacyOptionsForm`,
204
- `ConsentSnapshot.privacyOptionsRequirement`, and the types above. A hand-written
205
- `ConsentSnapshot` literal (a test mock) needs the new field.
206
-
207
- **Three behaviour changes:** `initialize()` after a declined or failed consent flow
208
- returns `blocked` and initializes nothing (0.3.0 returned `ready` and initialized); a
209
- consent gate closing while `ready` now reports `blocked` and cancels in-flight loads
210
- (0.3.0 stayed `ready` and a mounted banner kept requesting); and reopening a gate
211
- resumes with no second native initialization (0.3.0 initialized again), unless an
212
- init-time flag changed in between.
213
-
214
- **Fixed:** a `load()` in flight when ads are disabled, undecided or disposed now resolves
215
- (`disabled` / `undecided` / `unavailable`) instead of never settling.
216
-
217
- ## Upgrading 0.2.x → 0.3.0
218
-
219
- One compile-time change and one behaviour change.
220
-
221
- | Union | New member |
222
- |---|---|
223
- | `PlacementState` | `'throttled'` |
224
- | `LoadResult` | `{ status: 'throttled', retryAfterMs }` |
225
- | `AdEventType` | `'request_throttled'` |
226
-
227
- **The behaviour change:** banner placements now have a 60s floor between requests. If your
228
- banner lives inside a list row, a tab, or a view the user swipes between, it will report
229
- `throttled` after a quick navigation where 0.2.x silently issued another request. That was
230
- the bug; this is the fix. Read the throttle section above before setting
231
- `minRequestIntervalMs: 0`.
232
-
233
- Full-screen formats are unchanged — their default floor is `0`.
234
-
235
- ## Upgrading 0.1.0 → 0.2.0
236
-
237
- One required change, compile-time only: five unions gained members, so an exhaustive
238
- `switch` over any of them needs the new cases or a `default`.
239
-
240
- | Union | New members |
241
- |---|---|
242
- | `AdFormat` | `'app_open'` |
243
- | `InitStatus` | `'undecided'` |
244
- | `PlacementState` | `'undecided'` |
245
- | `PrivacyBlockReason` | `'consent_flow_required'`, `'consent_flow_error'` |
246
- | `AdsErrorCode` | `'undecided'`, `'ad_originated_return'`, `'consent_flow_required'`, `'consent_flow_failed'`, `'retry_out_of_bounds'` |
247
-
248
- Everything else is opt-in: consent, `'undecided'`, app-open and retry are all off or absent
249
- until configured. One behavior change needs no action — on an unavailable platform with
250
- ads also disabled, the reason is now `unsupported_platform` where 0.1.0 said `disabled`.
251
-
252
- ## Where the detail lives
253
-
254
- Read one section of `node_modules/@chipmobilesdk/rn-ads/README.md`:
255
-
256
- | Need | Section |
257
- |---|---|
258
- | First integration | `## Install`, `## Configure` |
259
- | Consent ordering | `## Initialize — consent comes first` |
260
- | Running the consent flow | `## Consent` |
261
- | A "Privacy options" entry, withdrawal and re-grant | `### Letting users change their choice` |
262
- | Rendering a banner | `## Banner` |
263
- | Full-screen formats | `## Interstitial and rewarded` |
264
- | App-open and ad-originated return | `## App open` |
265
- | Turning ads off, undecided, re-enabling | `## Turning ads off — three states` |
266
- | Retry and reconciling it against a bill | `## Retry` |
267
- | Request throttling, banners in lists | `## Request throttling` |
268
- | Why a request was refused | `## Why an ad did not show` |
269
- | Results, events, errors, timeouts, cleanup | `## State, events, and errors` |
270
- | Tests without a device | `## Testing` |
271
- | Permissions, Data safety, families | `## Install`, `## Release checklists` |
272
- | Build failure after a bridge upgrade | `## Compatibility` |
273
- | Migration detail | `## Versioning` |
274
- | What is deferred | `## Known limitations`, `## Extending to iOS` |
1
+ ---
2
+ name: sdk-rn-ads
3
+ description: "Advertising via @chipmobilesdk/rn-ads: AdMob banner/interstitial/rewarded/app-open behind a provider-neutral contract, placement keys, optional consent orchestration incl. the privacy options (change my consent) form, three-state enablement, bounded retry, per-environment test-ad substitution, per-placement request throttling. Use when adding ads to a screen, running a consent flow without importing AdMob, adding a Privacy options entry to Settings, deferring ads until a purchase check resolves, debugging an ad that never loads or reports throttled, placing a banner inside a list or a view the user swipes between, mapping ad units per environment, or answering a Data safety question about advertising."
4
+ metadata:
5
+ package: "@chipmobilesdk/rn-ads"
6
+ packageVersion: "0.4.0"
7
+ user-invocable: true
8
+ disable-model-invocation: false
9
+ ---
10
+
11
+ # @chipmobilesdk/rn-ads
12
+
13
+ Adaptive banner, interstitial, rewarded and app-open ads on Android via Google AdMob,
14
+ behind a contract that names no provider. The app names a placement key; the package owns
15
+ the request. Consuming code never imports the Google Mobile Ads SDK — including for
16
+ consent.
17
+
18
+ ## Use this when
19
+
20
+ - Adding a banner, interstitial, rewarded or app-open ad to a screen
21
+ - Running a consent flow in a region that requires one, without importing AdMob
22
+ - Letting a user change their consent later (a "Privacy options" entry in Settings)
23
+ - Deferring ads at startup until a purchase or entitlement check answers
24
+ - Turning ads back on after a subscription lapses or a refund lands
25
+ - Mapping AdMob ad units to placement keys per environment
26
+ - Debugging an ad that never loads, or a `blocked` / `undecided` / `unavailable` /
27
+ `ad_originated_return` / `provider_error`
28
+ - Answering a Data safety / App Privacy question about advertising
29
+
30
+ ## Do not use this for
31
+
32
+ - **Consent UI or copy** — the package *orchestrates* the provider's flow and normalizes
33
+ the outcome, but the form is drawn by the provider and the legal text is theirs. Timing,
34
+ surrounding copy, and where the "change your mind" entry lives stay yours.
35
+ - **Where or how often ads appear** — no frequency cap, no placement rule, no auto-insert.
36
+ The package never shows an ad on its own, app-open included.
37
+ - **Granting the reward** — it reports `reward_earned`; entitlement is the app's.
38
+ - **Anything about payments** — the undecided state exists *because* apps sell ad removal,
39
+ but the package receives a decision and never a reason, receipt or entitlement.
40
+ - iOS — not implemented; every call returns `unavailable` / `unsupported_platform`.
41
+
42
+ ## Nothing loads until consent and initialization complete
43
+
44
+ This is enforced, and it is the answer to almost every "the ad never appears". With the
45
+ gate withheld, **zero** provider calls occur — not even configuration. `childDirected`,
46
+ `underAgeOfConsent` and `maxAdContentRating` take effect only if they reach
47
+ `initialize()` *before* the provider starts; changing one later returns
48
+ `requires_reinitialization` rather than silently not applying.
49
+ Once you call `ads.consent.*`, the flow is part of that gate: `initialize()` returns
50
+ `blocked` · `consent_flow_required` until it permits, and the package initializes by
51
+ itself when it does — do not retry `initialize()` in a loop.
52
+ See `## Initialize — consent comes first` and `## Consent`.
53
+
54
+ ## "Change my consent" is covered — never `reset()` + re-show
55
+
56
+ New in 0.4.0. Show your Settings entry when `consent.getState().privacyOptionsRequirement`
57
+ is `'required'` (hide it on `'unknown'` and `'not_required'`), and call
58
+ `ads.consent.showPrivacyOptionsForm()` from it. Where the entry lives, and its copy, stay
59
+ yours. The result feeds the gate by itself. A withdrawal moves `ready` → `blocked` and
60
+ cancels in-flight loads; a re-grant resumes with **no second native initialization**.
61
+ `consent.reset()` is testing-only — it wipes the stored decision while the form is open.
62
+ See `### Letting users change their choice` under `## Consent`.
63
+
64
+ ## Three gates, one reason — check this order first
65
+
66
+ A refused request reports exactly one reason, and the precedence is fixed:
67
+ **platform availability → enablement → consent**, most immovable first. So
68
+ `unsupported_platform` on a disabled instance is reported as `unsupported_platform` —
69
+ turning ads on would not have helped. Full table in `## Why an ad did not show`.
70
+
71
+ ## Never gate `<AdsBanner>` on its own placement state
72
+
73
+ `AdsBanner` is what measures the container and starts the load, so it must be mounted
74
+ unconditionally. Rendering it only when the state is `loading`/`ready` deadlocks at
75
+ `idle` and no ad ever appears. Use the state for the *surrounding* placeholder and to
76
+ decide whether to hold or reclaim space — never to decide whether to mount the host. It
77
+ renders `null` on its own when there is nothing to show.
78
+
79
+ ## `undecided` is not `disabled`, and that is the whole point
80
+
81
+ `enabled` takes `true`, `false` or `'undecided'`. All three send zero requests except
82
+ `true`. `'undecided'` reports itself distinctly — in `getStatus()` and in the banner's
83
+ placement state — so an app that has not yet heard back from the store can **hold** the
84
+ banner's space instead of reclaiming it, and a paying user never sees an ad flash in and
85
+ out. `disabled` means a settled no; reclaim the space.
86
+
87
+ The package never resolves it for you. Leave it unresolved past `undecidedWarningMs`
88
+ (default 10s) and you get exactly one `config_warning`, never a stream.
89
+
90
+ ## `setEnabled` returns a result — read it
91
+
92
+ In 0.1.0 it returned `void` and re-enabling after a disable silently dropped the init
93
+ status, so every later `load` and `show` was refused with nothing to say so. That was a
94
+ defect and it is fixed. The result now says which happened:
95
+
96
+ - `resumed` — the provider was never un-initialized; load away, zero provider calls made.
97
+ - `requires_initialization` — call `initialize()` yourself. The package will **not** do it
98
+ implicitly, because that would hide an async, billable operation behind a setter.
99
+
100
+ Existing `ads.setEnabled(false);` call sites still compile untouched.
101
+
102
+ ## A banner in a list or a swipeable view gets throttled — by design
103
+
104
+ New in 0.3.0. A banner mounted inside a view the user navigates between — a day in a
105
+ calendar, a tab, a row that scrolls in and out — is unmounted and remounted by React on
106
+ every navigation, and **each remount is a billable request**. Ten quick swipes used to be
107
+ ten requests in a few seconds, which ad networks read as invalid traffic. The penalty is
108
+ account-level and no shipped fix recovers it.
109
+
110
+ So each placement now has a minimum interval between *requests*: **60s for a banner**
111
+ (AdMob's own refresh floor), **off for full-screen formats** — those are loaded by an
112
+ explicit call, not mounted by a renderer, so they never had the problem. A refusal reports
113
+ `throttled` on the placement and emits `request_throttled` with `retryAfterMs`.
114
+
115
+ Three things to know before you reach for `minRequestIntervalMs: 0`:
116
+
117
+ - **`throttled` is not `failed` and not a no-fill.** Nothing is wrong and nothing was
118
+ requested. Collapse the space if you like, but do not log it as an error.
119
+ - **Rotation is exempt.** A width re-adaptation inside one mount is not charged, so a
120
+ rotated screen keeps its banner.
121
+ - **The better fix is architectural.** Keep the banner outside the subtree that remounts
122
+ and the throttle never fires. It is a safety net, not a licence to ignore the problem.
123
+
124
+ This is **not** a frequency cap. It limits how often a placement may *ask*; how often you
125
+ *show* is yours, and the package deliberately ships no pacing (see `## Do not use this for`).
126
+
127
+ ## Five things not to "fix"
128
+
129
+ - **The bridge pin.** `react-native-google-mobile-ads` is peered at `>=16.0.3 <16.1.0`;
130
+ 16.1.0+ carries Kotlin 2.3 metadata that React Native 0.85's pinned compiler cannot
131
+ read, and the Android build fails outright. `0.2.0` and `0.4.0` needed no bump —
132
+ consent, app-open and privacy options are all already in `16.0.3`. Read
133
+ `## Compatibility` before widening it.
134
+ - **Test ads in dev.** Outside `production`, non-official ad units are replaced with
135
+ Google's test units and reported via `config_warning` / `getSubstitutedPlacements()`.
136
+ That is the safety guarantee working, not a misconfiguration. Never click a production
137
+ ad to verify anything.
138
+ - **Consent tools going quiet in production.** `consentTesting.debugGeography` and
139
+ `resetOnStart` are stripped at config validation in `production` and listed by
140
+ `getIgnoredConsentTools()`. Working as designed.
141
+ - **A declined consent still reporting `obtained` / `canRequestAds: true`.** That is UMP's
142
+ meaning, not a bug: `OBTAINED` means *an answer was recorded*, and non-personalized ads
143
+ stay servable. The package forwards the provider's verdict rather than inventing one.
144
+ - **A banner reporting `throttled` right after a navigation.** That is 0.3.0's request
145
+ floor working — see the section above. Lower it knowingly or move the banner out of the
146
+ remounting subtree; do not set `0` to make the symptom go away.
147
+
148
+ ## Retry is off unless you ask
149
+
150
+ No `retry` in config means the request count is byte-identical to 0.1.0 — a test in the
151
+ package proves it. When enabled it is bounded (default 2 retries, 2s then 4s, no jitter),
152
+ retries only errors already flagged `retryable`, and cancels everything on disable,
153
+ undecided, consent loss or `dispose()`. Every attempt emits `retry_scheduled` carrying the
154
+ load cycle's `correlationId`, because every attempt is a billable request.
155
+ Per-placement `retry: false` turns it off for one placement.
156
+
157
+ ## App-open: the one rule you cannot implement yourself
158
+
159
+ App-open shares the single full-screen lock with interstitial and rewarded, and expires on
160
+ its own shorter window (30 min default, vs 60). The part worth knowing: a show requested
161
+ on a return **from an ad** — or from a browser an ad opened — is refused with
162
+ `ad_originated_return`, distinct from `busy` and `not_ready`. Your app cannot tell that
163
+ return apart from any other; the package can. It cannot survive process death, and a
164
+ browser opened without a preceding click is undetectable. See `## App open`.
165
+
166
+ ## Public API (v0.5.0)
167
+
168
+ Runtime: `createAds` (throws `AdsConfigError` on a bad catalog or an out-of-range retry
169
+ policy; everything else returns a typed result rather than throwing) · `AdsProvider`,
170
+ `useAds` · `AdsBanner` · `usePlacementState`, `useAdsStatus`, `useAdsEvents` · `AdsError`,
171
+ `AdsConfigError`, `isAdsError`, `isFullScreenFormat` · `DEFAULT_LOAD_TIMEOUT_MS`,
172
+ `DEFAULT_SHOW_TIMEOUT_MS`, `DEFAULT_AD_VALIDITY_MS`, `DEFAULT_APP_OPEN_VALIDITY_MS`,
173
+ `DEFAULT_RETRY_POLICY`, `DEFAULT_UNDECIDED_WARNING_MS`, `LOAD_TIMEOUT_BOUNDS`,
174
+ `SHOW_TIMEOUT_BOUNDS`, `AD_VALIDITY_BOUNDS`, `APP_OPEN_VALIDITY_BOUNDS`, `RETRY_BOUNDS`,
175
+ `UNDECIDED_WARNING_BOUNDS`, `DEFAULT_BANNER_MIN_REQUEST_INTERVAL_MS`,
176
+ `DEFAULT_FULL_SCREEN_MIN_REQUEST_INTERVAL_MS`, `MIN_REQUEST_INTERVAL_BOUNDS`,
177
+ `defaultMinRequestIntervalMs` · `createFakeAdsEngine`, `createTestClock` from
178
+ `@chipmobilesdk/rn-ads/testing` (test doubles only).
179
+
180
+ Instance: `initialize`, `updatePrivacy`, `setEnabled`, `setEnablement`, `setAppActive`,
181
+ `consent`, `getStatus`, `subscribeStatus`, `load`, `show`, `getPlacementState`,
182
+ `subscribePlacement`, `subscribeEvents`, `getSubstitutedPlacements`,
183
+ `getIgnoredConsentTools`, `dispose`.
184
+
185
+ Consent namespace (`ads.consent.*`, opt-in — untouched, nothing starts):
186
+ `requestInfoUpdate`, `showFormIfRequired`, `showPrivacyOptionsForm`, `getState`,
187
+ `subscribe`, `reset`.
188
+
189
+ Types: `AdsInstance`, `AdsConfig`, `PlacementDefinition`, `PrivacyOptions`, `AdFormat`,
190
+ `FullScreenFormat`, `Environment`, `Enablement`, `EnablementResult`, `ConsentApi`,
191
+ `ConsentState`, `ConsentSnapshot`, `ConsentTestingOptions`, `PrivacyOptionsRequirement`,
192
+ `PrivacyOptionsFormResult`, `RetryPolicy`, `InitStatus`,
193
+ `PlacementState`, `InitResult`, `LoadResult`, `ShowResult`, `PrivacyUpdateResult`,
194
+ `Reward`, `AdEvent`, `AdEventType`, `AdEventListener`, `ConfigWarning`, `ImpressionSource`,
195
+ `AdsErrorCode`, `AdsOperation`, `TimeoutConfig`, `AdContentRating`, `PrivacyBlockReason`,
196
+ `UnavailableReason`, `Unsubscribe`, `AdsBannerProps`, `AdsProviderProps`.
197
+
198
+ Call `dispose()` when the owning screen unmounts; it is idempotent, and so is every
199
+ `Unsubscribe`.
200
+
201
+ ## Upgrading 0.3.x → 0.4.0
202
+
203
+ No code change: no existing union gained a member. New: `showPrivacyOptionsForm`,
204
+ `ConsentSnapshot.privacyOptionsRequirement`, and the types above. A hand-written
205
+ `ConsentSnapshot` literal (a test mock) needs the new field.
206
+
207
+ **Three behaviour changes:** `initialize()` after a declined or failed consent flow
208
+ returns `blocked` and initializes nothing (0.3.0 returned `ready` and initialized); a
209
+ consent gate closing while `ready` now reports `blocked` and cancels in-flight loads
210
+ (0.3.0 stayed `ready` and a mounted banner kept requesting); and reopening a gate
211
+ resumes with no second native initialization (0.3.0 initialized again), unless an
212
+ init-time flag changed in between.
213
+
214
+ **Fixed:** a `load()` in flight when ads are disabled, undecided or disposed now resolves
215
+ (`disabled` / `undecided` / `unavailable`) instead of never settling.
216
+
217
+ ## Upgrading 0.2.x → 0.3.0
218
+
219
+ One compile-time change and one behaviour change.
220
+
221
+ | Union | New member |
222
+ |---|---|
223
+ | `PlacementState` | `'throttled'` |
224
+ | `LoadResult` | `{ status: 'throttled', retryAfterMs }` |
225
+ | `AdEventType` | `'request_throttled'` |
226
+
227
+ **The behaviour change:** banner placements now have a 60s floor between requests. If your
228
+ banner lives inside a list row, a tab, or a view the user swipes between, it will report
229
+ `throttled` after a quick navigation where 0.2.x silently issued another request. That was
230
+ the bug; this is the fix. Read the throttle section above before setting
231
+ `minRequestIntervalMs: 0`.
232
+
233
+ Full-screen formats are unchanged — their default floor is `0`.
234
+
235
+ ## Upgrading 0.1.0 → 0.2.0
236
+
237
+ One required change, compile-time only: five unions gained members, so an exhaustive
238
+ `switch` over any of them needs the new cases or a `default`.
239
+
240
+ | Union | New members |
241
+ |---|---|
242
+ | `AdFormat` | `'app_open'` |
243
+ | `InitStatus` | `'undecided'` |
244
+ | `PlacementState` | `'undecided'` |
245
+ | `PrivacyBlockReason` | `'consent_flow_required'`, `'consent_flow_error'` |
246
+ | `AdsErrorCode` | `'undecided'`, `'ad_originated_return'`, `'consent_flow_required'`, `'consent_flow_failed'`, `'retry_out_of_bounds'` |
247
+
248
+ Everything else is opt-in: consent, `'undecided'`, app-open and retry are all off or absent
249
+ until configured. One behavior change needs no action — on an unavailable platform with
250
+ ads also disabled, the reason is now `unsupported_platform` where 0.1.0 said `disabled`.
251
+
252
+ ## Where the detail lives
253
+
254
+ Read one section of `node_modules/@chipmobilesdk/rn-ads/README.md`:
255
+
256
+ | Need | Section |
257
+ |---|---|
258
+ | First integration | `## Install`, `## Configure` |
259
+ | Consent ordering | `## Initialize — consent comes first` |
260
+ | Running the consent flow | `## Consent` |
261
+ | A "Privacy options" entry, withdrawal and re-grant | `### Letting users change their choice` |
262
+ | Rendering a banner | `## Banner` |
263
+ | Full-screen formats | `## Interstitial and rewarded` |
264
+ | App-open and ad-originated return | `## App open` |
265
+ | Turning ads off, undecided, re-enabling | `## Turning ads off — three states` |
266
+ | Retry and reconciling it against a bill | `## Retry` |
267
+ | Request throttling, banners in lists | `## Request throttling` |
268
+ | Why a request was refused | `## Why an ad did not show` |
269
+ | Results, events, errors, timeouts, cleanup | `## State, events, and errors` |
270
+ | Tests without a device | `## Testing` |
271
+ | Permissions, Data safety, families | `## Install`, `## Release checklists` |
272
+ | Build failure after a bridge upgrade | `## Compatibility` |
273
+ | Migration detail | `## Versioning` |
274
+ | What is deferred | `## Known limitations`, `## Extending to iOS` |