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