@chipmobilesdk/rn-ads 0.1.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.
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Public configuration and state types (FR-001, FR-003, FR-004).
3
+ *
4
+ * Nothing here names a platform or an ad provider. That is the whole point:
5
+ * adding an iOS adapter later contributes an optional `iosAdUnitId` and a second
6
+ * engine implementation, and changes nothing a consumer already wrote (FR-006).
7
+ *
8
+ * The package persists none of this. Every value below lives for the lifetime of
9
+ * one instance and is released by `dispose()`.
10
+ */
11
+
12
+ /** Formats supported in v1. App-open, native, and rewarded-interstitial are deferred. */
13
+ export type AdFormat = 'banner' | 'interstitial' | 'rewarded';
14
+
15
+ /** Formats that take over the screen, and so contend for the single-show lock. */
16
+ export type FullScreenFormat = Extract<AdFormat, 'interstitial' | 'rewarded'>;
17
+
18
+ export function isFullScreenFormat(format: AdFormat): format is FullScreenFormat {
19
+ return format === 'interstitial' || format === 'rewarded';
20
+ }
21
+
22
+ /**
23
+ * Declared by the app, because the package cannot depend on any particular
24
+ * configuration mechanism (FR-015). Cross-checked against the runtime dev flag
25
+ * so a debug build claiming `production` is caught rather than trusted (FR-012).
26
+ */
27
+ export type Environment = 'development' | 'test' | 'production';
28
+
29
+ /**
30
+ * One logical ad slot. `key` is the app's own stable name — `home_footer`, not
31
+ * an AdMob unit. Provider identifiers appear here in configuration and nowhere
32
+ * else in consumer code (FR-003, FR-007).
33
+ */
34
+ export interface PlacementDefinition {
35
+ readonly key: string;
36
+ readonly format: AdFormat;
37
+ readonly androidAdUnitId: string;
38
+ /**
39
+ * Additive placeholder for FR-006. Unused in v1 and never read by the Android
40
+ * adapter — present so that adding iOS is a configuration change rather than a
41
+ * type change for every existing consumer.
42
+ */
43
+ readonly iosAdUnitId?: string;
44
+ }
45
+
46
+ /** Bounds exist because an unbounded timeout is the same defect as no timeout. */
47
+ export interface TimeoutConfig {
48
+ readonly load?: number;
49
+ readonly show?: number;
50
+ }
51
+
52
+ export interface AdsConfig {
53
+ /** FR-009. When false, every operation is a safe no-op reporting `disabled`. */
54
+ readonly enabled: boolean;
55
+ readonly environment: Environment;
56
+ readonly placements: readonly PlacementDefinition[];
57
+ readonly timeouts?: TimeoutConfig;
58
+ /** How long a loaded full-screen ad stays presentable (FR-034). */
59
+ readonly adValidityMs?: number;
60
+ /** Ignored entirely in `production` (FR-014). */
61
+ readonly testDeviceIds?: readonly string[];
62
+ }
63
+
64
+ /** Content ratings, normalized. Maps onto the provider's own rating scale. */
65
+ export type AdContentRating = 'G' | 'PG' | 'T' | 'MA';
66
+
67
+ /**
68
+ * Supplied before initialization (FR-016). Never logged, never persisted
69
+ * (FR-021). Everything except `canRequestAds` must reach the provider before it
70
+ * initializes, which is why changing one afterwards reports
71
+ * `requires_reinitialization` rather than pretending it applied (FR-020).
72
+ */
73
+ export interface PrivacyOptions {
74
+ /** The gate itself. False means no request may be made, for any reason. */
75
+ readonly canRequestAds: boolean;
76
+ /** Distinguishes "the user said no" from "we have not asked yet" (FR-018). */
77
+ readonly consentUndetermined?: boolean;
78
+ readonly childDirected?: boolean;
79
+ readonly underAgeOfConsent?: boolean;
80
+ readonly maxAdContentRating?: AdContentRating;
81
+ /** When false, the package requests non-personalized ads. */
82
+ readonly personalizedAds?: boolean;
83
+ }
84
+
85
+ export type PrivacyBlockReason = 'consent_required' | 'consent_undetermined';
86
+
87
+ export type UnavailableReason = 'unsupported_platform' | 'provider_missing';
88
+
89
+ /** FR-023. One per instance, observable without holding a provider object. */
90
+ export type InitStatus =
91
+ | 'not_started'
92
+ | 'blocked'
93
+ | 'initializing'
94
+ | 'ready'
95
+ | 'disabled'
96
+ | 'unavailable'
97
+ | 'failed';
98
+
99
+ /**
100
+ * FR-026. One per placement. Banners never reach `consumed` — they are
101
+ * continuous, not single-use.
102
+ */
103
+ export type PlacementState =
104
+ | 'idle'
105
+ | 'loading'
106
+ | 'ready'
107
+ | 'showing'
108
+ | 'consumed'
109
+ | 'failed'
110
+ | 'unavailable';
111
+
112
+ /** FR-047. The provider's reward descriptor, normalized. */
113
+ export interface Reward {
114
+ readonly type: string;
115
+ readonly amount: number;
116
+ }
117
+
118
+ /** FR-005. Returned by every subscription; safe to call more than once. */
119
+ export type Unsubscribe = () => void;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Catalog and configuration validation (FR-008), plus the fail-closed check
3
+ * that substitution cannot cover (FR-012).
4
+ *
5
+ * Configuration faults throw rather than returning a result. They mean the app
6
+ * is wired wrong, and no runtime status could describe a usable outcome — every
7
+ * other failure in this package is a typed result instead.
8
+ *
9
+ * Every message names the offending placement key. An error that says only
10
+ * "invalid configuration" makes the integrator hunt through a catalog by hand.
11
+ */
12
+
13
+ import { AdsConfigError } from '../diagnostics/errors';
14
+ import { resolvePlacementAdUnits, type ResolvedCatalog } from './resolveAdUnit';
15
+ import { resolveTimeouts, type ResolvedTimeouts } from '../core/timers';
16
+ import type { AdFormat, AdsConfig, Environment, PlacementDefinition } from './types';
17
+
18
+ const FORMATS: ReadonlySet<string> = new Set<AdFormat>(['banner', 'interstitial', 'rewarded']);
19
+
20
+ export interface ValidateOptions {
21
+ /**
22
+ * Whether this is a debug build. Injected rather than read from `__DEV__`
23
+ * directly so the fail-closed rule is testable without manipulating a global
24
+ * the test runner also depends on.
25
+ */
26
+ readonly isDevBuild: boolean;
27
+ }
28
+
29
+ export interface ResolvedConfig {
30
+ readonly enabled: boolean;
31
+ readonly environment: Environment;
32
+ readonly placements: ReadonlyMap<string, PlacementDefinition>;
33
+ readonly catalog: ResolvedCatalog;
34
+ readonly timeouts: ResolvedTimeouts;
35
+ /** Empty in production regardless of what was configured (FR-014). */
36
+ readonly testDeviceIds: readonly string[];
37
+ }
38
+
39
+ export function validateConfig(config: AdsConfig, options: ValidateOptions): ResolvedConfig {
40
+ // FR-012. Substitution deliberately does not apply in production, so a debug
41
+ // build declaring `production` is the one case it cannot make safe: it would
42
+ // send real requests from a build that should never generate traffic. Fail
43
+ // closed rather than continue.
44
+ if (config.environment === 'production' && options.isDevBuild) {
45
+ throw new AdsConfigError('production_in_dev_build', {
46
+ message:
47
+ 'environment is "production" but this is a development build. ' +
48
+ 'Non-production builds must declare "development" or "test" so the package can ' +
49
+ 'substitute official Google test ad units and keep production traffic impossible.',
50
+ });
51
+ }
52
+
53
+ if (!Array.isArray(config.placements) || config.placements.length === 0) {
54
+ throw new AdsConfigError('invalid_config', {
55
+ message: 'placements must be a non-empty array; the package can show nothing without one',
56
+ });
57
+ }
58
+
59
+ const byKey = new Map<string, PlacementDefinition>();
60
+
61
+ for (const placement of config.placements) {
62
+ const key = typeof placement?.key === 'string' ? placement.key.trim() : '';
63
+
64
+ if (!key) {
65
+ throw new AdsConfigError('invalid_config', {
66
+ message: 'every placement needs a non-empty key',
67
+ });
68
+ }
69
+
70
+ if (byKey.has(key)) {
71
+ // Rejecting the whole catalog rather than picking a winner: a repeated
72
+ // key means the app's own naming produced two placements for one slot,
73
+ // and the package has no basis to guess which was meant.
74
+ throw new AdsConfigError('duplicate_placement_key', {
75
+ message: `duplicate placement key "${key}"`,
76
+ placementKey: key,
77
+ });
78
+ }
79
+
80
+ if (!FORMATS.has(placement.format)) {
81
+ throw new AdsConfigError('invalid_config', {
82
+ message: `placement "${key}" has unsupported format "${String(placement.format)}"; v1 supports banner, interstitial, and rewarded`,
83
+ placementKey: key,
84
+ });
85
+ }
86
+
87
+ if (!placement.androidAdUnitId || !placement.androidAdUnitId.trim()) {
88
+ throw new AdsConfigError('missing_ad_unit_id', {
89
+ message: `placement "${key}" is missing androidAdUnitId`,
90
+ placementKey: key,
91
+ format: placement.format,
92
+ });
93
+ }
94
+
95
+ byKey.set(key, { ...placement, key });
96
+ }
97
+
98
+ const timeouts = resolveTimeouts(config.timeouts, config.adValidityMs);
99
+ const catalog = resolvePlacementAdUnits(Array.from(byKey.values()), config.environment);
100
+
101
+ return {
102
+ enabled: config.enabled !== false,
103
+ environment: config.environment,
104
+ placements: byKey,
105
+ catalog,
106
+ timeouts,
107
+ // FR-014: debug and test-device settings must not be able to alter
108
+ // production behavior, so they are dropped rather than carried and
109
+ // conditionally ignored somewhere further down.
110
+ testDeviceIds: config.environment === 'production' ? [] : [...(config.testDeviceIds ?? [])],
111
+ };
112
+ }