@chipmobilesdk/rn-ads 0.3.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,71 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6
6
 
7
7
  The event catalog, error codes, state names, and result unions are **public API**. Renaming or removing any of them is a major change.
8
8
 
9
+ ## [0.5.0] — 2026-11-01
10
+
11
+ 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.
12
+
13
+ ### Fixed
14
+
15
+ - **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.
16
+ - **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.
17
+ - **`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()`.
18
+
19
+ - **`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.
20
+
21
+ ### Changed
22
+
23
+ - **`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`.
24
+ - **`updatePrivacy()` on a disposed instance returns `unavailable` / `provider_missing`**, as `initialize()` and `load()` already did. It returned `disabled`.
25
+ - `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`.
26
+
27
+ ## [0.4.0] — 2026-10-12
28
+
29
+ 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.
30
+
31
+ **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).
32
+
33
+ ### Added
34
+
35
+ - **Privacy options: the "change my choice" form** (`ads.consent.showPrivacyOptionsForm()`). Presents the provider's privacy options form and returns a typed `PrivacyOptionsFormResult`, never throwing:
36
+ - `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.
37
+ - `not_required`, `info_update_required`, `busy`: refusals that present nothing.
38
+ - `failed`, carrying an `AdsError` (`consent_flow_failed`).
39
+ - `unavailable`: `unsupported_platform` on iOS, `provider_missing` after `dispose()`.
40
+
41
+ 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.
42
+
43
+ - **`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.
44
+
45
+ - **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.
46
+
47
+ - **`./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' }`.
48
+
49
+ - Exported types `PrivacyOptionsFormResult` and `PrivacyOptionsRequirement`.
50
+
51
+ 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.
52
+
53
+ 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.
54
+
55
+ ### Changed
56
+
57
+ - **`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.
58
+
59
+ - **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.
60
+
61
+ - **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.
62
+
63
+ ### Fixed
64
+
65
+ - **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.
66
+ - **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.
67
+
68
+ ### Not added, deliberately
69
+
70
+ - **No Settings entry, copy or offer timing.** Those are policy, and they stay the app's.
71
+ - **No region detection of the package's own.** The requirement is the provider's answer.
72
+ - **iOS** stays `unavailable` / `unsupported_platform`.
73
+
9
74
  ## [0.3.0] — 2026-10-10
10
75
 
11
76
  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.
@@ -56,7 +121,7 @@ Additive minor release closing one gap that surfaced when `0.2.x` met an app pla
56
121
 
57
122
  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.
58
123
 
59
- **Upgrading requires one change, and it is compile-time only**: see [Changed](#changed) and the migration guide in the README.
124
+ **Upgrading requires one change, and it is compile-time only**: see [Changed](#changed-2) and the migration guide in the README.
60
125
 
61
126
  ### Added
62
127