@chipmobilesdk/rn-ads 0.1.0 → 0.2.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 +57 -0
- package/README.md +310 -11
- package/package.json +1 -1
- package/skills/sdk-rn-ads/SKILL.md +98 -98
- package/src/config/types.ts +170 -8
- package/src/config/validate.ts +83 -7
- package/src/core/adOriginatedReturn.ts +134 -0
- package/src/core/adsInstance.ts +565 -42
- package/src/core/consentFlow.ts +167 -0
- package/src/core/enablement.ts +76 -0
- package/src/core/gates.ts +91 -0
- package/src/core/initMachine.ts +73 -7
- package/src/core/retryPolicy.ts +127 -0
- package/src/core/timers.ts +109 -10
- package/src/diagnostics/errors.ts +27 -3
- package/src/diagnostics/events.ts +41 -1
- package/src/engine/admobEngine.ts +144 -2
- package/src/engine/port.ts +44 -0
- package/src/engine/testAdUnits.ts +3 -0
- package/src/index.ts +23 -1
- package/src/react/AdsBanner.tsx +23 -1
- package/src/testing/index.ts +155 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ad-originated return detection (FR-128).
|
|
3
|
+
*
|
|
4
|
+
* This is the reason app-open belongs in the package rather than in the app.
|
|
5
|
+
* Every other app-open rule — when to show one, how often, on which screens — is
|
|
6
|
+
* policy the app owns. This one is not policy; it is a fact only the package
|
|
7
|
+
* holds. When a user dismisses a full-screen ad, or comes back from a browser an
|
|
8
|
+
* ad opened, the app sees an ordinary return to the foreground and has no way to
|
|
9
|
+
* tell it apart from any other. Showing an app-open ad on that return means
|
|
10
|
+
* showing an ad immediately after an ad, which is both a poor experience and the
|
|
11
|
+
* kind of thing store review notices.
|
|
12
|
+
*
|
|
13
|
+
* The package knows, because it holds the full-screen lock and it sees the click
|
|
14
|
+
* events. So it refuses, with a reason distinct from `busy` and `not_ready` so
|
|
15
|
+
* the app can tell what happened.
|
|
16
|
+
*
|
|
17
|
+
* **Bounded on both sides, deliberately.** Arming has to be tied to something
|
|
18
|
+
* that actually happened, and it has to expire:
|
|
19
|
+
*
|
|
20
|
+
* - Armed when the provider reports a full-screen ad opened, or on a click —
|
|
21
|
+
* that second case is a browser the ad opened, which is the only signal the
|
|
22
|
+
* provider gives for it. Arming is deliberately not tied to the app
|
|
23
|
+
* reporting that it went to the background; see `noteFullScreenShown`.
|
|
24
|
+
* - Disarmed without firing if the app was away longer than
|
|
25
|
+
* `AD_RETURN_MAX_AWAY_MS`. Someone who left, did something else, and came
|
|
26
|
+
* back an hour later is not returning from an ad, and treating them as if
|
|
27
|
+
* they were would refuse a legitimate show.
|
|
28
|
+
* - Refusals last `AD_RETURN_GRACE_MS` after the return, then stop. The window
|
|
29
|
+
* only needs to cover the moment right after the return, where an app-open
|
|
30
|
+
* show would otherwise fire. A window that outlived that moment would start
|
|
31
|
+
* refusing shows that have nothing to do with the ad.
|
|
32
|
+
*
|
|
33
|
+
* Three limits are worth stating plainly, because they are limits rather than
|
|
34
|
+
* defects and the documentation must not imply otherwise:
|
|
35
|
+
*
|
|
36
|
+
* 1. It cannot survive process death. If the OS kills the app while an ad is
|
|
37
|
+
* on screen, the return is a cold start and nothing links it to the ad.
|
|
38
|
+
* 2. It depends on the app reporting `setAppActive` faithfully — an obligation
|
|
39
|
+
* 0.1.0 already imposes for staleness re-evaluation, not a new one.
|
|
40
|
+
* 3. A browser opened by an ad without a preceding click event is undetectable.
|
|
41
|
+
*
|
|
42
|
+
* Pure state. No listener, no timer — it is driven entirely by transitions the
|
|
43
|
+
* package already observes, which is why `dispose()` has nothing extra to
|
|
44
|
+
* release for it.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { AD_CLICK_WINDOW_MS, AD_RETURN_GRACE_MS, AD_RETURN_MAX_AWAY_MS } from './timers';
|
|
48
|
+
|
|
49
|
+
export interface AdOriginatedTracker {
|
|
50
|
+
/** A full-screen ad is on screen, per the provider's own `opened` callback. */
|
|
51
|
+
noteFullScreenShown(nowMs: number): void;
|
|
52
|
+
/** Record an ad click — the only signal that an ad opened a browser. */
|
|
53
|
+
noteClick(nowMs: number): void;
|
|
54
|
+
/** The app went to the background. Kept for the click case's timing. */
|
|
55
|
+
noteBackground(nowMs: number): void;
|
|
56
|
+
/** The app came back. Decides whether this return was ad-originated. */
|
|
57
|
+
noteForeground(nowMs: number): void;
|
|
58
|
+
/** Whether an app-open show should be refused right now. */
|
|
59
|
+
isRefusing(nowMs: number): boolean;
|
|
60
|
+
/** Drops all state. For dispose, and for tests asserting a clean slate. */
|
|
61
|
+
reset(): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createAdOriginatedTracker(): AdOriginatedTracker {
|
|
65
|
+
let lastClickAtMs: number | null = null;
|
|
66
|
+
let armedAtMs: number | null = null;
|
|
67
|
+
let refusalUntilMs: number | null = null;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
/**
|
|
71
|
+
* Arming happens here — when the provider says an ad is on screen — and
|
|
72
|
+
* deliberately **not** when the app reports going to the background.
|
|
73
|
+
*
|
|
74
|
+
* That was the first design and it does not survive a real device. React
|
|
75
|
+
* Native delivers app-state changes over the bridge, and while the app is
|
|
76
|
+
* backgrounded its JS thread is throttled, so the `background` event is
|
|
77
|
+
* routinely processed only once the app is already returning. By then the
|
|
78
|
+
* ad has been dismissed and the full-screen lock released, so sampling the
|
|
79
|
+
* lock at that moment reports "no ad was showing" and the return is let
|
|
80
|
+
* through — the exact case FR-128 exists to catch. Arming on a fact the
|
|
81
|
+
* provider reports, rather than on the timeliness of an event, removes the
|
|
82
|
+
* race entirely.
|
|
83
|
+
*/
|
|
84
|
+
noteFullScreenShown(nowMs) {
|
|
85
|
+
armedAtMs = nowMs;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
noteClick(nowMs) {
|
|
89
|
+
lastClickAtMs = nowMs;
|
|
90
|
+
// A click is itself grounds to arm: the ad may be opening a browser,
|
|
91
|
+
// and if it is, no `opened` callback will follow.
|
|
92
|
+
armedAtMs = nowMs;
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
noteBackground(nowMs) {
|
|
96
|
+
// Only meaningful for the browser case, and only as a refinement: a
|
|
97
|
+
// background that closely follows a click is that browser opening.
|
|
98
|
+
if (lastClickAtMs !== null && nowMs - lastClickAtMs <= AD_CLICK_WINDOW_MS) {
|
|
99
|
+
armedAtMs = nowMs;
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
noteForeground(nowMs) {
|
|
104
|
+
if (armedAtMs === null) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const away = nowMs - armedAtMs;
|
|
108
|
+
armedAtMs = null;
|
|
109
|
+
lastClickAtMs = null;
|
|
110
|
+
if (away > AD_RETURN_MAX_AWAY_MS) {
|
|
111
|
+
// Too long to still be attributable to the ad. Not a refusal.
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
refusalUntilMs = nowMs + AD_RETURN_GRACE_MS;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
isRefusing(nowMs) {
|
|
118
|
+
if (refusalUntilMs === null) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (nowMs >= refusalUntilMs) {
|
|
122
|
+
refusalUntilMs = null;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
reset() {
|
|
129
|
+
lastClickAtMs = null;
|
|
130
|
+
armedAtMs = null;
|
|
131
|
+
refusalUntilMs = null;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|