@chipmobilesdk/rn-ads 0.4.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.
@@ -1,125 +1,144 @@
1
- /**
2
- * The internal engine port (NFR-008, FR-006).
3
- *
4
- * This interface names no platform and no ad provider. That is what makes an
5
- * iOS adapter an additive change: it implements this, and nothing in the public
6
- * contract moves. `core/` talks only to this shape; the AdMob bridge exists
7
- * behind exactly one implementation of it, in `admobEngine.ts`.
8
- *
9
- * Banners are absent on purpose. The provider owns a banner's whole lifecycle
10
- * through a React component, so `react/AdsBanner` adapts those callbacks
11
- * directly rather than routing them through a handle model that would have to
12
- * pretend a banner is a load-then-show resource.
13
- */
14
-
15
- import type { AdContentRating, FullScreenFormat, Reward, Unsubscribe } from '../config/types';
16
-
17
- /** Privacy state as the engine needs it — no consumer-facing shape leaks in. */
18
- export interface EnginePrivacy {
19
- readonly childDirected?: boolean;
20
- readonly underAgeOfConsent?: boolean;
21
- readonly maxAdContentRating?: AdContentRating;
22
- readonly personalizedAds?: boolean;
23
- readonly testDeviceIds?: readonly string[];
24
- }
25
-
26
- /**
27
- * Provider callbacks, normalized to the vocabulary the core state machines use.
28
- * `rewarded` and `dismissed` are separate members because they are separate
29
- * facts that can arrive in either order (FR-049).
30
- */
31
- export type EngineAdEvent =
32
- | { readonly type: 'loaded' }
33
- | { readonly type: 'failed'; readonly cause: unknown }
34
- | { readonly type: 'opened' }
35
- | { readonly type: 'clicked' }
36
- | { readonly type: 'dismissed' }
37
- | { readonly type: 'rewarded'; readonly reward: Reward };
38
-
39
- /** An opaque reference to one loaded ad. Never exposed publicly. */
40
- export interface EngineAdHandle {
41
- readonly id: string;
42
- }
43
-
44
- /**
45
- * Consent status as the engine reports it — the provider's own vocabulary,
46
- * lowercased, with nothing provider-shaped attached. `core/consentFlow.ts`
47
- * normalizes this into the public `ConsentState`; keeping the two separate is
48
- * what lets an iOS adapter map a different mechanism onto the same public shape
49
- * without either side changing.
50
- */
51
- export interface EngineConsentInfo {
52
- readonly status: 'unknown' | 'required' | 'not_required' | 'obtained';
53
- readonly canRequestAds: boolean;
54
- readonly isFormAvailable: boolean;
55
- /** Whether the provider requires a "change my choice" entry point (FR-150). */
56
- readonly privacyOptions: 'unknown' | 'required' | 'not_required';
57
- }
58
-
59
- export interface EngineConsentOptions {
60
- /** Already stripped by config validation when the environment is production. */
61
- readonly debugGeography?: 'disabled' | 'eea' | 'not_eea' | 'regulated_us_state' | 'other';
62
- readonly underAgeOfConsent?: boolean;
63
- readonly testDeviceIds?: readonly string[];
64
- }
65
-
66
- /**
67
- * The provider's consent platform, behind the same boundary as everything else.
68
- *
69
- * It is a separate member of `AdsEngine` rather than more methods on it because
70
- * it is a genuinely separate native module - the ads SDK and the consent SDK
71
- * initialize independently and neither requires the other.
72
- *
73
- * The package orchestrates; it never renders. `showFormIfRequired` asks the
74
- * provider to present its own form and reports what came back (FR-103).
75
- */
76
- export interface ConsentPort {
77
- /** Refreshes consent information, applying any testing options. */
78
- requestInfoUpdate(options: EngineConsentOptions): Promise<EngineConsentInfo>;
79
- /** Presents the provider's form if one is required; a no-op otherwise. */
80
- showFormIfRequired(): Promise<EngineConsentInfo>;
81
- /**
82
- * Presents the provider's privacy options form, through which a user changes
83
- * an answer already given (FR-151). Resolves when the form closes, whether or
84
- * not anything changed; rejects if it could not be presented. The caller has
85
- * already checked the requirement, so the adapter does not re-check.
86
- */
87
- showPrivacyOptionsForm(): Promise<EngineConsentInfo>;
88
- /** Last known information without contacting the network. */
89
- getInfo(): Promise<EngineConsentInfo>;
90
- /** Clears the stored decision. Inert in production by configuration. */
91
- reset(): void;
92
- }
93
-
94
- export interface AdsEngine {
95
- /**
96
- * True when this platform has a working adapter. Checked before anything
97
- * else so an unsupported platform never loads the provider at all (FR-025).
98
- */
99
- isAvailable(): boolean;
100
-
101
- /**
102
- * Applies privacy configuration and then initializes the provider. The order
103
- * matters: child-directed and under-age flags only take effect if they reach
104
- * the provider before it initializes (FR-017, R9).
105
- *
106
- * The caller guarantees this is invoked at most once per instance; the
107
- * engine does not have to be idempotent itself.
108
- */
109
- initialize(privacy: EnginePrivacy): Promise<void>;
110
-
111
- /** Creates and begins loading one full-screen ad. Resolves on request, not on load. */
112
- createFullScreen(format: FullScreenFormat, adUnitId: string): Promise<EngineAdHandle>;
113
-
114
- /** Presents a loaded ad. Rejects if the provider refuses to present. */
115
- show(handle: EngineAdHandle): Promise<void>;
116
-
117
- /** Provider callbacks for one handle. Returns an unsubscribe (FR-005). */
118
- subscribe(handle: EngineAdHandle, listener: (event: EngineAdEvent) => void): Unsubscribe;
119
-
120
- /** Releases the handle's provider resources. Safe to call more than once. */
121
- dispose(handle: EngineAdHandle): void;
122
-
123
- /** The provider's consent platform. Only touched if the app opts in (FR-102). */
124
- readonly consent: ConsentPort;
125
- }
1
+ /**
2
+ * The internal engine port (NFR-008, FR-006).
3
+ *
4
+ * This interface names no platform and no ad provider. That is what makes an
5
+ * iOS adapter an additive change: it implements this, and nothing in the public
6
+ * contract moves. `core/` talks only to this shape; the AdMob bridge exists
7
+ * behind exactly one implementation of it, in `admobEngine.ts`.
8
+ *
9
+ * Banners are absent on purpose. The provider owns a banner's whole lifecycle
10
+ * through a React component, so `react/AdsBanner` adapts those callbacks
11
+ * directly rather than routing them through a handle model that would have to
12
+ * pretend a banner is a load-then-show resource.
13
+ */
14
+
15
+ import type { AdContentRating, FullScreenFormat, Reward, Unsubscribe } from '../config/types';
16
+
17
+ /** Privacy state as the engine needs it — no consumer-facing shape leaks in. */
18
+ export interface EnginePrivacy {
19
+ readonly childDirected?: boolean;
20
+ readonly underAgeOfConsent?: boolean;
21
+ readonly maxAdContentRating?: AdContentRating;
22
+ readonly personalizedAds?: boolean;
23
+ readonly testDeviceIds?: readonly string[];
24
+ }
25
+
26
+ /**
27
+ * Privacy that applies to each ad request rather than to initialization.
28
+ *
29
+ * `personalizedAds` is here, not only in `EnginePrivacy`, because the provider
30
+ * reads it per request and `updatePrivacy` may change it without
31
+ * reinitializing: a request must carry the value current at request time.
32
+ */
33
+ export interface EngineRequestOptions {
34
+ readonly personalizedAds: boolean;
35
+ }
36
+
37
+ /**
38
+ * Provider callbacks, normalized to the vocabulary the core state machines use.
39
+ * `rewarded` and `dismissed` are separate members because they are separate
40
+ * facts that can arrive in either order (FR-049).
41
+ */
42
+ export type EngineAdEvent =
43
+ | { readonly type: 'loaded' }
44
+ | { readonly type: 'failed'; readonly cause: unknown }
45
+ | { readonly type: 'opened' }
46
+ | { readonly type: 'clicked' }
47
+ | { readonly type: 'dismissed' }
48
+ | { readonly type: 'rewarded'; readonly reward: Reward };
49
+
50
+ /** An opaque reference to one loaded ad. Never exposed publicly. */
51
+ export interface EngineAdHandle {
52
+ readonly id: string;
53
+ }
54
+
55
+ /**
56
+ * Consent status as the engine reports it — the provider's own vocabulary,
57
+ * lowercased, with nothing provider-shaped attached. `core/consentFlow.ts`
58
+ * normalizes this into the public `ConsentState`; keeping the two separate is
59
+ * what lets an iOS adapter map a different mechanism onto the same public shape
60
+ * without either side changing.
61
+ */
62
+ export interface EngineConsentInfo {
63
+ readonly status: 'unknown' | 'required' | 'not_required' | 'obtained';
64
+ readonly canRequestAds: boolean;
65
+ readonly isFormAvailable: boolean;
66
+ /** Whether the provider requires a "change my choice" entry point (FR-150). */
67
+ readonly privacyOptions: 'unknown' | 'required' | 'not_required';
68
+ }
69
+
70
+ export interface EngineConsentOptions {
71
+ /** Already stripped by config validation when the environment is production. */
72
+ readonly debugGeography?: 'disabled' | 'eea' | 'not_eea' | 'regulated_us_state' | 'other';
73
+ readonly underAgeOfConsent?: boolean;
74
+ readonly testDeviceIds?: readonly string[];
75
+ }
76
+
77
+ /**
78
+ * The provider's consent platform, behind the same boundary as everything else.
79
+ *
80
+ * It is a separate member of `AdsEngine` rather than more methods on it because
81
+ * it is a genuinely separate native module - the ads SDK and the consent SDK
82
+ * initialize independently and neither requires the other.
83
+ *
84
+ * The package orchestrates; it never renders. `showFormIfRequired` asks the
85
+ * provider to present its own form and reports what came back (FR-103).
86
+ */
87
+ export interface ConsentPort {
88
+ /** Refreshes consent information, applying any testing options. */
89
+ requestInfoUpdate(options: EngineConsentOptions): Promise<EngineConsentInfo>;
90
+ /** Presents the provider's form if one is required; a no-op otherwise. */
91
+ showFormIfRequired(): Promise<EngineConsentInfo>;
92
+ /**
93
+ * Presents the provider's privacy options form, through which a user changes
94
+ * an answer already given (FR-151). Resolves when the form closes, whether or
95
+ * not anything changed; rejects if it could not be presented. The caller has
96
+ * already checked the requirement, so the adapter does not re-check.
97
+ */
98
+ showPrivacyOptionsForm(): Promise<EngineConsentInfo>;
99
+ /** Last known information without contacting the network. */
100
+ getInfo(): Promise<EngineConsentInfo>;
101
+ /** Clears the stored decision. Inert in production by configuration. */
102
+ reset(): void;
103
+ }
104
+
105
+ export interface AdsEngine {
106
+ /**
107
+ * True when this platform has a working adapter. Checked before anything
108
+ * else so an unsupported platform never loads the provider at all (FR-025).
109
+ */
110
+ isAvailable(): boolean;
111
+
112
+ /**
113
+ * Applies privacy configuration and then initializes the provider. The order
114
+ * matters: child-directed and under-age flags only take effect if they reach
115
+ * the provider before it initializes (FR-017, R9).
116
+ *
117
+ * The caller guarantees this is invoked at most once per instance; the
118
+ * engine does not have to be idempotent itself.
119
+ */
120
+ initialize(privacy: EnginePrivacy): Promise<void>;
121
+
122
+ /**
123
+ * Creates and begins loading one full-screen ad. Resolves on request, not on
124
+ * load. `request` is optional so engines written against 0.4.0 still satisfy
125
+ * the port; an engine that ignores it requests personalized ads.
126
+ */
127
+ createFullScreen(
128
+ format: FullScreenFormat,
129
+ adUnitId: string,
130
+ request?: EngineRequestOptions,
131
+ ): Promise<EngineAdHandle>;
132
+
133
+ /** Presents a loaded ad. Rejects if the provider refuses to present. */
134
+ show(handle: EngineAdHandle): Promise<void>;
135
+
136
+ /** Provider callbacks for one handle. Returns an unsubscribe (FR-005). */
137
+ subscribe(handle: EngineAdHandle, listener: (event: EngineAdEvent) => void): Unsubscribe;
138
+
139
+ /** Releases the handle's provider resources. Safe to call more than once. */
140
+ dispose(handle: EngineAdHandle): void;
141
+
142
+ /** The provider's consent platform. Only touched if the app opts in (FR-102). */
143
+ readonly consent: ConsentPort;
144
+ }
@@ -1,61 +1,61 @@
1
- /**
2
- * Google's official test ad units (FR-013).
3
- *
4
- * PRIVATE. Never exported from `src/index.ts`.
5
- *
6
- * FR-013 forbids the package from carrying production identifiers, credentials,
7
- * or host-app rules, and explicitly permits official test identifiers provided
8
- * they are documented as such — this file is that documentation.
9
- *
10
- * They are hard-coded rather than read from configuration on purpose: FR-011's
11
- * substitution guarantee must hold even when the app's configuration is the
12
- * thing that is wrong, so it cannot depend on the app supplying anything. They
13
- * are also not re-exported from the provider bridge's own `TestIds`, because
14
- * that would put a provider symbol on this package's dependency path for a
15
- * value that is really just four documented constants (FR-002).
16
- *
17
- * Source: Google Mobile Ads "Test ads" documentation, cross-checked against the
18
- * bridge's `src/TestIds.ts` at version 16.0.3. These identifiers are public,
19
- * are the same for every publisher, and always fill.
20
- */
21
-
22
- import type { AdFormat } from '../config/types';
23
-
24
- /** Android. iOS units are added alongside these when the iOS adapter lands (FR-006). */
25
- const ANDROID_TEST_AD_UNITS: Readonly<Record<AdFormat, string>> = {
26
- // The adaptive-banner unit, not the fixed-size one, because adaptive is the
27
- // format this package actually requests (FR-038).
28
- banner: 'ca-app-pub-3940256099942544/9214589741',
29
- interstitial: 'ca-app-pub-3940256099942544/1033173712',
30
- rewarded: 'ca-app-pub-3940256099942544/5224354917',
31
- // Added in 0.2.0 alongside the app-open format. Cross-checked against the
32
- // bridge's own `TestIds.APP_OPEN` at 16.0.3.
33
- app_open: 'ca-app-pub-3940256099942544/9257395921',
34
- };
35
-
36
- export function testAdUnitFor(format: AdFormat): string {
37
- return ANDROID_TEST_AD_UNITS[format];
38
- }
39
-
40
- /**
41
- * Whether an identifier is one of ours.
42
- *
43
- * This is the structural test that drives substitution: anything that is not a
44
- * known official test unit is treated as production-owned. Deliberately not a
45
- * parse of AdMob's publisher-id format — a rule like that becomes wrong the day
46
- * Google changes the format, and being wrong here means either sending a
47
- * production request or blocking a legitimate one.
48
- */
49
- export function isOfficialTestAdUnit(adUnitId: string): boolean {
50
- for (const value of Object.values(ANDROID_TEST_AD_UNITS)) {
51
- if (value === adUnitId) {
52
- return true;
53
- }
54
- }
55
- return false;
56
- }
57
-
58
- /** Exposed for the package's own tests and for documentation generation. */
59
- export function allTestAdUnits(): Readonly<Record<AdFormat, string>> {
60
- return ANDROID_TEST_AD_UNITS;
61
- }
1
+ /**
2
+ * Google's official test ad units (FR-013).
3
+ *
4
+ * PRIVATE. Never exported from `src/index.ts`.
5
+ *
6
+ * FR-013 forbids the package from carrying production identifiers, credentials,
7
+ * or host-app rules, and explicitly permits official test identifiers provided
8
+ * they are documented as such — this file is that documentation.
9
+ *
10
+ * They are hard-coded rather than read from configuration on purpose: FR-011's
11
+ * substitution guarantee must hold even when the app's configuration is the
12
+ * thing that is wrong, so it cannot depend on the app supplying anything. They
13
+ * are also not re-exported from the provider bridge's own `TestIds`, because
14
+ * that would put a provider symbol on this package's dependency path for a
15
+ * value that is really just four documented constants (FR-002).
16
+ *
17
+ * Source: Google Mobile Ads "Test ads" documentation, cross-checked against the
18
+ * bridge's `src/TestIds.ts` at version 16.0.3. These identifiers are public,
19
+ * are the same for every publisher, and always fill.
20
+ */
21
+
22
+ import type { AdFormat } from '../config/types';
23
+
24
+ /** Android. iOS units are added alongside these when the iOS adapter lands (FR-006). */
25
+ const ANDROID_TEST_AD_UNITS: Readonly<Record<AdFormat, string>> = {
26
+ // The adaptive-banner unit, not the fixed-size one, because adaptive is the
27
+ // format this package actually requests (FR-038).
28
+ banner: 'ca-app-pub-3940256099942544/9214589741',
29
+ interstitial: 'ca-app-pub-3940256099942544/1033173712',
30
+ rewarded: 'ca-app-pub-3940256099942544/5224354917',
31
+ // Added in 0.2.0 alongside the app-open format. Cross-checked against the
32
+ // bridge's own `TestIds.APP_OPEN` at 16.0.3.
33
+ app_open: 'ca-app-pub-3940256099942544/9257395921',
34
+ };
35
+
36
+ export function testAdUnitFor(format: AdFormat): string {
37
+ return ANDROID_TEST_AD_UNITS[format];
38
+ }
39
+
40
+ /**
41
+ * Whether an identifier is one of ours.
42
+ *
43
+ * This is the structural test that drives substitution: anything that is not a
44
+ * known official test unit is treated as production-owned. Deliberately not a
45
+ * parse of AdMob's publisher-id format — a rule like that becomes wrong the day
46
+ * Google changes the format, and being wrong here means either sending a
47
+ * production request or blocking a legitimate one.
48
+ */
49
+ export function isOfficialTestAdUnit(adUnitId: string): boolean {
50
+ for (const value of Object.values(ANDROID_TEST_AD_UNITS)) {
51
+ if (value === adUnitId) {
52
+ return true;
53
+ }
54
+ }
55
+ return false;
56
+ }
57
+
58
+ /** Exposed for the package's own tests and for documentation generation. */
59
+ export function allTestAdUnits(): Readonly<Record<AdFormat, string>> {
60
+ return ANDROID_TEST_AD_UNITS;
61
+ }
@@ -190,23 +190,20 @@ export function AdsBanner({ placementKey, onStateChange, style }: AdsBannerProps
190
190
  // eslint-disable-next-line react-hooks/exhaustive-deps
191
191
  }, [ads, provider, status, resolution, containerWidth, cycle]);
192
192
 
193
- const onLayout = useCallback(
194
- (event: LayoutChangeEvent) => {
195
- const width = Math.round(event.nativeEvent.layout.width);
196
- setContainerWidth(previous => {
197
- if (previous === width) {
198
- return previous;
199
- }
200
- // FR-038: a genuine width or orientation change re-adapts. The
201
- // rounding above stops sub-pixel layout jitter from doing so.
202
- if (previous > 0 && width > 0) {
203
- setCycle(value => value + 1);
204
- }
205
- return width;
206
- });
207
- },
208
- [],
209
- );
193
+ const onLayout = useCallback((event: LayoutChangeEvent) => {
194
+ const width = Math.round(event.nativeEvent.layout.width);
195
+ setContainerWidth(previous => {
196
+ if (previous === width) {
197
+ return previous;
198
+ }
199
+ // FR-038: a genuine width or orientation change re-adapts. The
200
+ // rounding above stops sub-pixel layout jitter from doing so.
201
+ if (previous > 0 && width > 0) {
202
+ setCycle(value => value + 1);
203
+ }
204
+ return width;
205
+ });
206
+ }, []);
210
207
 
211
208
  const emit = useCallback(
212
209
  (event: Parameters<NonNullable<typeof ads>['internals']['emit']>[0]) => {
@@ -309,6 +306,8 @@ export function AdsBanner({ placementKey, onStateChange, style }: AdsBannerProps
309
306
 
310
307
  const { BannerAd, BannerAdSize } = provider;
311
308
  const adUnitId = resolution?.ok ? resolution.adUnitId : null;
309
+ // The provider's polarity is the inverse of the consumer-facing option.
310
+ const requestNonPersonalizedAdsOnly = resolution?.ok ? !resolution.personalizedAds : false;
312
311
 
313
312
  return (
314
313
  <View style={style} onLayout={onLayout} testID={`ads-banner-${placementKey}`}>
@@ -317,6 +316,7 @@ export function AdsBanner({ placementKey, onStateChange, style }: AdsBannerProps
317
316
  key={`${placementKey}-${cycle}`}
318
317
  unitId={adUnitId}
319
318
  size={BannerAdSize.ANCHORED_ADAPTIVE_BANNER}
319
+ requestOptions={{ requestNonPersonalizedAdsOnly }}
320
320
  onAdLoaded={handleLoaded}
321
321
  onAdFailedToLoad={handleFailed}
322
322
  onAdImpression={handleImpression}
@@ -1,57 +1,57 @@
1
- /**
2
- * Instance context (US1).
3
- *
4
- * Supplies one `AdsInstance` to the tree so `AdsBanner` and the hooks can reach
5
- * it without every screen threading it through props.
6
- *
7
- * The provider does not create the instance. The app does, and passes it in —
8
- * because `createAds` throws on a misconfigured catalog (FR-008, FR-012), and a
9
- * throw during render is a far worse place to discover that than a throw at
10
- * startup where the app can decide what to do about it.
11
- */
12
-
13
- import React, { createContext, useContext, useEffect, type ReactNode } from 'react';
14
- import { AppState, type AppStateStatus } from 'react-native';
15
-
16
- import type { AdsInstance } from '../core/adsInstance';
17
-
18
- const AdsContext = createContext<AdsInstance | null>(null);
19
-
20
- export interface AdsProviderProps {
21
- readonly instance: AdsInstance;
22
- /**
23
- * Whether the provider forwards foreground/background transitions to the
24
- * instance. On by default: FR-034's "never show while inactive" and
25
- * "re-evaluate a stale ad on return" both depend on the instance being told.
26
- * Turn it off only when the app already drives `setAppActive` itself.
27
- */
28
- readonly trackAppState?: boolean;
29
- readonly children: ReactNode;
30
- }
31
-
32
- export function AdsProvider({ instance, trackAppState = true, children }: AdsProviderProps) {
33
- useEffect(() => {
34
- if (!trackAppState) {
35
- return;
36
- }
37
- const subscription = AppState.addEventListener('change', (status: AppStateStatus) => {
38
- instance.setAppActive(status === 'active');
39
- });
40
- return () => {
41
- subscription.remove();
42
- };
43
- }, [instance, trackAppState]);
44
-
45
- return <AdsContext.Provider value={instance}>{children}</AdsContext.Provider>;
46
- }
47
-
48
- /**
49
- * Returns the instance, or null outside a provider.
50
- *
51
- * Null rather than a throw on purpose: a screen that renders a banner should
52
- * degrade to rendering nothing when ads are not wired up, not take the screen
53
- * down with it (NFR-005).
54
- */
55
- export function useAds(): AdsInstance | null {
56
- return useContext(AdsContext);
57
- }
1
+ /**
2
+ * Instance context (US1).
3
+ *
4
+ * Supplies one `AdsInstance` to the tree so `AdsBanner` and the hooks can reach
5
+ * it without every screen threading it through props.
6
+ *
7
+ * The provider does not create the instance. The app does, and passes it in —
8
+ * because `createAds` throws on a misconfigured catalog (FR-008, FR-012), and a
9
+ * throw during render is a far worse place to discover that than a throw at
10
+ * startup where the app can decide what to do about it.
11
+ */
12
+
13
+ import React, { createContext, useContext, useEffect, type ReactNode } from 'react';
14
+ import { AppState, type AppStateStatus } from 'react-native';
15
+
16
+ import type { AdsInstance } from '../core/adsInstance';
17
+
18
+ const AdsContext = createContext<AdsInstance | null>(null);
19
+
20
+ export interface AdsProviderProps {
21
+ readonly instance: AdsInstance;
22
+ /**
23
+ * Whether the provider forwards foreground/background transitions to the
24
+ * instance. On by default: FR-034's "never show while inactive" and
25
+ * "re-evaluate a stale ad on return" both depend on the instance being told.
26
+ * Turn it off only when the app already drives `setAppActive` itself.
27
+ */
28
+ readonly trackAppState?: boolean;
29
+ readonly children: ReactNode;
30
+ }
31
+
32
+ export function AdsProvider({ instance, trackAppState = true, children }: AdsProviderProps) {
33
+ useEffect(() => {
34
+ if (!trackAppState) {
35
+ return;
36
+ }
37
+ const subscription = AppState.addEventListener('change', (status: AppStateStatus) => {
38
+ instance.setAppActive(status === 'active');
39
+ });
40
+ return () => {
41
+ subscription.remove();
42
+ };
43
+ }, [instance, trackAppState]);
44
+
45
+ return <AdsContext.Provider value={instance}>{children}</AdsContext.Provider>;
46
+ }
47
+
48
+ /**
49
+ * Returns the instance, or null outside a provider.
50
+ *
51
+ * Null rather than a throw on purpose: a screen that renders a banner should
52
+ * degrade to rendering nothing when ads are not wired up, not take the screen
53
+ * down with it (NFR-005).
54
+ */
55
+ export function useAds(): AdsInstance | null {
56
+ return useContext(AdsContext);
57
+ }