react-native-purchases-admob 0.0.0-stage → 10.15.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/src/state.ts ADDED
@@ -0,0 +1,109 @@
1
+ import { AdCaptureMethod, AdMediatorName } from "react-native-purchases";
2
+ import type {
3
+ AdFormat,
4
+ RewardedAdTrackingMetadata,
5
+ } from "react-native-purchases";
6
+ import type { ResponseInfo } from "react-native-google-mobile-ads";
7
+
8
+ /**
9
+ * Per-ad bookkeeping, kept in a weak side table so the adapter never changes
10
+ * the lifetime of the `react-native-google-mobile-ads` objects it tracks.
11
+ */
12
+ export class TrackedAdState {
13
+ loadPlacement: string | null;
14
+ showPlacement: string | null = null;
15
+ /**
16
+ * Either AdMob's response id (read once the ad loads) or, for ads created
17
+ * with reward verification, the id generated before the request so the SSV
18
+ * token and every tracking event share one impression id.
19
+ */
20
+ impressionId: string = "";
21
+ networkName: string | null = null;
22
+ clientTransactionId: string | null = null;
23
+ rewardVerificationDelivered = false;
24
+ loadedOnce = false;
25
+
26
+ constructor(
27
+ readonly adFormat: AdFormat,
28
+ readonly adUnitId: string,
29
+ placement?: string | null,
30
+ public pinnedImpressionId: string | null = null
31
+ ) {
32
+ this.loadPlacement = placement ?? null;
33
+ if (pinnedImpressionId) {
34
+ this.impressionId = pinnedImpressionId;
35
+ }
36
+ }
37
+
38
+ get placement(): string | null {
39
+ return this.showPlacement ?? this.loadPlacement;
40
+ }
41
+
42
+ updateFromResponse(info: ResponseInfo | null | undefined): void {
43
+ if (!this.pinnedImpressionId) {
44
+ this.impressionId = info?.responseId ?? "";
45
+ }
46
+ this.networkName =
47
+ info?.loadedAdapterResponse?.adapterClassName ??
48
+ info?.adapterClassName ??
49
+ null;
50
+ }
51
+
52
+ get trackingMetadata(): RewardedAdTrackingMetadata {
53
+ return {
54
+ networkName: this.networkName,
55
+ mediatorName: AdMediatorName.adMob,
56
+ adFormat: this.adFormat,
57
+ placement: this.placement,
58
+ adUnitId: this.adUnitId,
59
+ impressionId: this.impressionId,
60
+ captureMethod: AdCaptureMethod.reactNativeAdMobAdapter,
61
+ } as RewardedAdTrackingMetadata;
62
+ }
63
+ }
64
+
65
+ const states = new WeakMap<object, TrackedAdState>();
66
+
67
+ export function stateFor(ad: object): TrackedAdState | undefined {
68
+ return states.get(ad);
69
+ }
70
+
71
+ export function setState(ad: object, state: TrackedAdState): TrackedAdState {
72
+ states.set(ad, state);
73
+ return state;
74
+ }
75
+
76
+ /**
77
+ * UUID v4. Uses `crypto` when the runtime or a polyfill such as
78
+ * `react-native-get-random-values` provides it; Hermes has none by default,
79
+ * so it falls back to `Math.random`, which is enough for a correlation key.
80
+ */
81
+ interface CryptoLike {
82
+ randomUUID?: () => string;
83
+ getRandomValues?: (array: Uint8Array) => Uint8Array;
84
+ }
85
+
86
+ export function generateImpressionId(): string {
87
+ const cryptoObj = (globalThis as { crypto?: CryptoLike }).crypto;
88
+ if (cryptoObj?.randomUUID) {
89
+ return cryptoObj.randomUUID();
90
+ }
91
+ const bytes = new Uint8Array(16);
92
+ if (cryptoObj?.getRandomValues) {
93
+ cryptoObj.getRandomValues(bytes);
94
+ } else {
95
+ for (let i = 0; i < bytes.length; i++) {
96
+ bytes[i] = Math.floor(Math.random() * 256);
97
+ }
98
+ }
99
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
100
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
101
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0"));
102
+ return [
103
+ hex.slice(0, 4).join(""),
104
+ hex.slice(4, 6).join(""),
105
+ hex.slice(6, 8).join(""),
106
+ hex.slice(8, 10).join(""),
107
+ hex.slice(10, 16).join(""),
108
+ ].join("-");
109
+ }
package/src/tracker.ts ADDED
@@ -0,0 +1,200 @@
1
+ import Purchases, {
2
+ AdCaptureMethod,
3
+ AdMediatorName,
4
+ AdRevenuePrecision,
5
+ } from "react-native-purchases";
6
+ import type {
7
+ AdDisplayedData,
8
+ AdFailedToLoadData,
9
+ AdFormat,
10
+ AdLoadedData,
11
+ AdOpenedData,
12
+ AdRevenueData,
13
+ AdRewardEarnedUnverifiedData,
14
+ } from "react-native-purchases";
15
+ import type { PaidEvent } from "react-native-google-mobile-ads";
16
+ import { Platform } from "react-native";
17
+
18
+ import { TrackedAdState } from "./state";
19
+
20
+ export function logWarning(message: string): void {
21
+ // tslint:disable-next-line:no-console
22
+ console.warn(`[react-native-purchases-admob] ${message}`);
23
+ }
24
+
25
+ // The data types mark this field @internal, hence the assertions below.
26
+ const captureMethod = AdCaptureMethod.reactNativeAdMobAdapter;
27
+
28
+ /**
29
+ * `react-native-google-mobile-ads` reports load failures with a string reason
30
+ * rather than the platform SDK's numeric error code, and the two SDKs number
31
+ * their codes differently. Each table inverts the library's own native mapping
32
+ * for that platform (ReactNativeGoogleMobileAdsCommon.java and
33
+ * RNGoogleMobileAdsOwnedMappers.m), so the emitted number matches what the
34
+ * native integration on the same platform reports.
35
+ */
36
+ const androidErrorCodes: Record<string, number> = {
37
+ "internal-error": 0,
38
+ "invalid-request": 1,
39
+ "network-error": 2,
40
+ "no-fill": 3,
41
+ "app-id-missing": 8,
42
+ "mediation-no-fill": 9,
43
+ "request-id-mismatch": 10,
44
+ "invalid-ad-string": 11,
45
+ };
46
+
47
+ const iosErrorCodes: Record<string, number> = {
48
+ "invalid-request": 0,
49
+ "no-fill": 1,
50
+ "network-error": 2,
51
+ "server-error": 3,
52
+ timeout: 5,
53
+ "mediation-data-error": 7,
54
+ "mediation-adapter-error": 8,
55
+ "mediation-invalid-ad-size": 10,
56
+ "internal-error": 11,
57
+ "invalid-argument": 12,
58
+ "ad-already-used": 19,
59
+ "app-id-missing": 20,
60
+ "invalid-ad-string": 21,
61
+ };
62
+
63
+ export function mediatorErrorCode(reason: string | undefined): number | null {
64
+ if (!reason) {
65
+ return null;
66
+ }
67
+ const table = Platform.OS === "ios" ? iosErrorCodes : androidErrorCodes;
68
+ const mapped = table[reason];
69
+ if (mapped !== undefined) {
70
+ return mapped;
71
+ }
72
+ const numeric = Number(reason);
73
+ return Number.isInteger(numeric) ? numeric : null;
74
+ }
75
+
76
+ export function revenueMicrosFrom(paid: PaidEvent): number {
77
+ if (paid.valueMicros != null) {
78
+ const micros = Number(paid.valueMicros);
79
+ if (Number.isFinite(micros)) {
80
+ return Math.round(micros);
81
+ }
82
+ }
83
+ return Math.round(paid.value * 1_000_000);
84
+ }
85
+
86
+ export function precisionFrom(precision: number): AdRevenuePrecision {
87
+ switch (precision) {
88
+ case 1:
89
+ return AdRevenuePrecision.estimated;
90
+ case 2:
91
+ return AdRevenuePrecision.publisherDefined;
92
+ case 3:
93
+ return AdRevenuePrecision.exact;
94
+ default:
95
+ return AdRevenuePrecision.unknown;
96
+ }
97
+ }
98
+
99
+ function swallow(event: string, promise: Promise<void>): void {
100
+ promise.catch((e) => logWarning(`Could not track ${event}: ${e}`));
101
+ }
102
+
103
+ export const AdMobTracker = {
104
+ trackLoaded(state: TrackedAdState): void {
105
+ swallow(
106
+ "ad loaded",
107
+ Purchases.adTracker.trackAdLoaded({
108
+ networkName: state.networkName,
109
+ mediatorName: AdMediatorName.adMob,
110
+ adFormat: state.adFormat,
111
+ placement: state.loadPlacement,
112
+ adUnitId: state.adUnitId,
113
+ impressionId: state.impressionId,
114
+ captureMethod,
115
+ } as AdLoadedData)
116
+ );
117
+ },
118
+
119
+ trackFailedToLoad(
120
+ adUnitId: string,
121
+ adFormat: AdFormat,
122
+ placement: string | null,
123
+ reason: string | undefined
124
+ ): void {
125
+ swallow(
126
+ "ad failed to load",
127
+ Purchases.adTracker.trackAdFailedToLoad({
128
+ mediatorName: AdMediatorName.adMob,
129
+ adFormat,
130
+ placement,
131
+ adUnitId,
132
+ mediatorErrorCode: mediatorErrorCode(reason),
133
+ captureMethod,
134
+ } as AdFailedToLoadData)
135
+ );
136
+ },
137
+
138
+ trackDisplayed(state: TrackedAdState): void {
139
+ swallow(
140
+ "ad displayed",
141
+ Purchases.adTracker.trackAdDisplayed({
142
+ networkName: state.networkName,
143
+ mediatorName: AdMediatorName.adMob,
144
+ adFormat: state.adFormat,
145
+ placement: state.placement,
146
+ adUnitId: state.adUnitId,
147
+ impressionId: state.impressionId,
148
+ captureMethod,
149
+ } as AdDisplayedData)
150
+ );
151
+ },
152
+
153
+ trackOpened(state: TrackedAdState): void {
154
+ swallow(
155
+ "ad opened",
156
+ Purchases.adTracker.trackAdOpened({
157
+ networkName: state.networkName,
158
+ mediatorName: AdMediatorName.adMob,
159
+ adFormat: state.adFormat,
160
+ placement: state.placement,
161
+ adUnitId: state.adUnitId,
162
+ impressionId: state.impressionId,
163
+ captureMethod,
164
+ } as AdOpenedData)
165
+ );
166
+ },
167
+
168
+ trackRewardEarnedUnverified(state: TrackedAdState): void {
169
+ swallow(
170
+ "reward earned",
171
+ Purchases.adTracker.trackAdRewardEarnedUnverified({
172
+ networkName: state.networkName,
173
+ mediatorName: AdMediatorName.adMob,
174
+ adFormat: state.adFormat,
175
+ placement: state.placement,
176
+ adUnitId: state.adUnitId,
177
+ impressionId: state.impressionId,
178
+ captureMethod,
179
+ } as AdRewardEarnedUnverifiedData)
180
+ );
181
+ },
182
+
183
+ trackRevenue(state: TrackedAdState, paid: PaidEvent): void {
184
+ swallow(
185
+ "ad revenue",
186
+ Purchases.adTracker.trackAdRevenue({
187
+ networkName: state.networkName,
188
+ mediatorName: AdMediatorName.adMob,
189
+ adFormat: state.adFormat,
190
+ placement: state.placement,
191
+ adUnitId: state.adUnitId,
192
+ impressionId: state.impressionId,
193
+ revenueMicros: revenueMicrosFrom(paid),
194
+ currency: paid.currency,
195
+ precision: precisionFrom(paid.precision),
196
+ captureMethod,
197
+ } as AdRevenueData)
198
+ );
199
+ },
200
+ };