@gabagool-sdk/react-native 0.0.0-stage → 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.
- package/CHANGELOG.md +10 -0
- package/LICENSE +190 -0
- package/README.md +103 -2
- package/dist/Banner.d.ts +56 -0
- package/dist/Banner.d.ts.map +1 -0
- package/dist/Banner.js +48 -0
- package/dist/adapter.d.ts +26 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +162 -0
- package/dist/bannerFlow.d.ts +35 -0
- package/dist/bannerFlow.d.ts.map +1 -0
- package/dist/bannerFlow.js +62 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +9 -0
- package/dist/facade.d.ts +53 -0
- package/dist/facade.d.ts.map +1 -0
- package/dist/facade.js +104 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/installInfo.d.ts +17 -0
- package/dist/installInfo.d.ts.map +1 -0
- package/dist/installInfo.js +38 -0
- package/dist/lifecycle.d.ts +16 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +30 -0
- package/dist/storage.d.ts +8 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +24 -0
- package/dist/transport.d.ts +5 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +17 -0
- package/package.json +41 -4
- package/src/Banner.tsx +82 -0
- package/src/adapter.ts +193 -0
- package/src/bannerFlow.ts +73 -0
- package/src/errors.ts +12 -0
- package/src/facade.ts +150 -0
- package/src/index.ts +8 -0
- package/src/installInfo.ts +43 -0
- package/src/lifecycle.ts +35 -0
- package/src/storage.ts +15 -0
- package/src/transport.ts +17 -0
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import mobileAds, {
|
|
2
|
+
AdEventType,
|
|
3
|
+
AppOpenAd,
|
|
4
|
+
GAMInterstitialAd,
|
|
5
|
+
InterstitialAd,
|
|
6
|
+
RewardedAd,
|
|
7
|
+
RewardedAdEventType,
|
|
8
|
+
RevenuePrecisions,
|
|
9
|
+
} from 'react-native-google-mobile-ads';
|
|
10
|
+
import { numericCode } from './errors.js';
|
|
11
|
+
import {
|
|
12
|
+
networkOf,
|
|
13
|
+
type AdFormat,
|
|
14
|
+
type AdHandle,
|
|
15
|
+
type AdNetworkAdapter,
|
|
16
|
+
type FloorKV,
|
|
17
|
+
type LoadResult,
|
|
18
|
+
type ShowResult,
|
|
19
|
+
} from '@gabagool-sdk/core';
|
|
20
|
+
type RequestOptions = { customTargeting?: Record<string, string | number | (string | number)[]> };
|
|
21
|
+
|
|
22
|
+
type Remove = () => void;
|
|
23
|
+
type EventPayload = Error & { code?: string; reason?: string; phase?: 'load' | 'show' };
|
|
24
|
+
type PaidPayload = {
|
|
25
|
+
currency: string;
|
|
26
|
+
precision: RevenuePrecisions;
|
|
27
|
+
value: number;
|
|
28
|
+
valueMicros?: string | null;
|
|
29
|
+
responseInfo?: { loadedAdapterResponse?: { adSourceName?: string | null } | null };
|
|
30
|
+
};
|
|
31
|
+
type NativeFullscreen = {
|
|
32
|
+
load(): void;
|
|
33
|
+
show(): Promise<void>;
|
|
34
|
+
destroy(): void;
|
|
35
|
+
addAdEventListener(type: string, listener: (payload: never) => void): Remove;
|
|
36
|
+
responseInfo: { loadedAdapterResponse?: { adSourceName?: string | null } | null } | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const precisionName = (value: RevenuePrecisions): string => ({
|
|
40
|
+
[RevenuePrecisions.ESTIMATED]: 'estimated',
|
|
41
|
+
[RevenuePrecisions.PRECISE]: 'precise',
|
|
42
|
+
[RevenuePrecisions.PUBLISHER_PROVIDED]: 'publisher_provided',
|
|
43
|
+
[RevenuePrecisions.UNKNOWN]: 'unknown',
|
|
44
|
+
})[value] ?? 'unknown';
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/** Adapter do core para a API estável do Invertase 17.x. */
|
|
48
|
+
export class ReactNativeGmaAdapter implements AdNetworkAdapter {
|
|
49
|
+
private sequence = 0;
|
|
50
|
+
private readonly nativeAds = new Map<string, NativeFullscreen>();
|
|
51
|
+
private readonly bannerFailures = new Map<string, number>();
|
|
52
|
+
|
|
53
|
+
/** Banner é componente: o resultado real chega por onAdLoaded/onAdFailedToLoad (core.trackBannerLoad). */
|
|
54
|
+
readonly bannerLoadReportedByComponent = true;
|
|
55
|
+
|
|
56
|
+
/** Falha do initialize() não trava o app (como a Dart): segue e os loads reportam seus próprios erros. */
|
|
57
|
+
async initialize(): Promise<void> {
|
|
58
|
+
try { await mobileAds().initialize(); } catch { /* segue sem travar */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
loadInterstitial(adUnit: string, floorKV?: FloorKV): Promise<LoadResult> {
|
|
62
|
+
return this.loadFullscreen('interstitial', adUnit, floorKV);
|
|
63
|
+
}
|
|
64
|
+
loadRewarded(adUnit: string, floorKV?: FloorKV): Promise<LoadResult> {
|
|
65
|
+
return this.loadFullscreen('rewarded', adUnit, floorKV);
|
|
66
|
+
}
|
|
67
|
+
loadAppOpen(adUnit: string, floorKV?: FloorKV): Promise<LoadResult> {
|
|
68
|
+
return this.loadFullscreen('app_open', adUnit, floorKV);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async loadBanner(adUnit: string, _floorKV?: FloorKV): Promise<LoadResult> {
|
|
72
|
+
const failures = this.bannerFailures.get(adUnit) ?? 0;
|
|
73
|
+
if (failures > 0) {
|
|
74
|
+
if (failures === 1) this.bannerFailures.delete(adUnit);
|
|
75
|
+
else this.bannerFailures.set(adUnit, failures - 1);
|
|
76
|
+
return { status: 'no_fill', code: 3 };
|
|
77
|
+
}
|
|
78
|
+
// Banner é um componente no Invertase. O handle descreve o componente que a fachada renderizará.
|
|
79
|
+
return { status: 'fill', ad: this.handle('banner', adUnit) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Native ainda não portado para RN (Invertase NativeAd): reporta erro explícito, nunca fill falso. */
|
|
83
|
+
async loadNative(_adUnit: string, _floorKV?: FloorKV): Promise<LoadResult> {
|
|
84
|
+
return { status: 'error', error: 'native_not_supported_react_native' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
markBannerFailed(ad: AdHandle): void {
|
|
88
|
+
this.bannerFailures.set(ad.adUnit, (this.bannerFailures.get(ad.adUnit) ?? 0) + 1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async setTestDeviceIds(ids: string[]): Promise<void> {
|
|
92
|
+
await mobileAds().setRequestConfiguration({ testDeviceIdentifiers: [...ids] });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
dispose(ad: AdHandle): void {
|
|
96
|
+
const native = this.nativeAds.get(ad.id);
|
|
97
|
+
native?.destroy();
|
|
98
|
+
this.nativeAds.delete(ad.id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
show(ad: AdHandle): Promise<ShowResult> {
|
|
102
|
+
const native = this.nativeAds.get(ad.id);
|
|
103
|
+
if (!native) return Promise.resolve({ status: 'error', error: 'ad_handle_not_found' });
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
let earned = false;
|
|
106
|
+
let paid: Omit<ShowResult, 'status'> = {};
|
|
107
|
+
let settled = false;
|
|
108
|
+
const removers: Remove[] = [];
|
|
109
|
+
const finish = (result: ShowResult) => {
|
|
110
|
+
if (settled) return;
|
|
111
|
+
settled = true;
|
|
112
|
+
removers.forEach((remove) => remove());
|
|
113
|
+
native.destroy();
|
|
114
|
+
this.nativeAds.delete(ad.id);
|
|
115
|
+
resolve(result);
|
|
116
|
+
};
|
|
117
|
+
removers.push(native.addAdEventListener(AdEventType.PAID, ((event: PaidPayload) => {
|
|
118
|
+
const exact = event.valueMicros == null ? undefined : Number(event.valueMicros);
|
|
119
|
+
paid = {
|
|
120
|
+
paidValue: event.value,
|
|
121
|
+
...(Number.isFinite(exact) ? { paidMicros: exact } : { paidMicros: Math.round(event.value * 1_000_000) }),
|
|
122
|
+
currency: event.currency,
|
|
123
|
+
precision: precisionName(event.precision),
|
|
124
|
+
adSource: event.responseInfo?.loadedAdapterResponse?.adSourceName
|
|
125
|
+
?? native.responseInfo?.loadedAdapterResponse?.adSourceName ?? undefined,
|
|
126
|
+
};
|
|
127
|
+
}) as (payload: never) => void));
|
|
128
|
+
if (ad.format === 'rewarded') {
|
|
129
|
+
removers.push(native.addAdEventListener(RewardedAdEventType.EARNED_REWARD, (() => { earned = true; }) as (payload: never) => void));
|
|
130
|
+
}
|
|
131
|
+
removers.push(native.addAdEventListener(AdEventType.ERROR, ((error: EventPayload) =>
|
|
132
|
+
finish({ status: 'error', error: error.message || 'show_failed', ...paid })) as (payload: never) => void));
|
|
133
|
+
removers.push(native.addAdEventListener(AdEventType.CLOSED, (() =>
|
|
134
|
+
finish({ status: ad.format === 'rewarded' && earned ? 'earned' : 'closed', ...paid })) as (payload: never) => void));
|
|
135
|
+
try { void native.show().catch((error: unknown) => finish({ status: 'error', error: String(error), ...paid })); }
|
|
136
|
+
catch (error) { finish({ status: 'error', error: String(error), ...paid }); }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private loadFullscreen(format: Exclude<AdFormat, 'banner'>, adUnit: string, floorKV?: FloorKV): Promise<LoadResult> {
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
let native: NativeFullscreen;
|
|
143
|
+
try {
|
|
144
|
+
const request = this.requestOptions(adUnit, floorKV);
|
|
145
|
+
const created = format === 'app_open'
|
|
146
|
+
? AppOpenAd.createForAdRequest(adUnit, request)
|
|
147
|
+
: format === 'rewarded'
|
|
148
|
+
? RewardedAd.createForAdRequest(adUnit, request)
|
|
149
|
+
: networkOf(adUnit) === 'gam'
|
|
150
|
+
? GAMInterstitialAd.createForAdRequest(adUnit, request)
|
|
151
|
+
: InterstitialAd.createForAdRequest(adUnit, request);
|
|
152
|
+
native = created as unknown as NativeFullscreen;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
resolve({ status: 'error', error: String(error) });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let settled = false;
|
|
158
|
+
const removers: Remove[] = [];
|
|
159
|
+
const finish = (result: LoadResult) => {
|
|
160
|
+
if (settled) return;
|
|
161
|
+
settled = true;
|
|
162
|
+
removers.forEach((remove) => remove());
|
|
163
|
+
if (result.status !== 'fill') native.destroy();
|
|
164
|
+
resolve(result);
|
|
165
|
+
};
|
|
166
|
+
const loadedEvent = format === 'rewarded' ? RewardedAdEventType.LOADED : AdEventType.LOADED;
|
|
167
|
+
removers.push(native.addAdEventListener(loadedEvent, (() => {
|
|
168
|
+
const handle = this.handle(format, adUnit, this.requestFloor(adUnit, floorKV));
|
|
169
|
+
this.nativeAds.set(handle.id, native);
|
|
170
|
+
finish({ status: 'fill', ad: handle });
|
|
171
|
+
}) as (payload: never) => void));
|
|
172
|
+
removers.push(native.addAdEventListener(AdEventType.ERROR, ((error: EventPayload) => {
|
|
173
|
+
const code = numericCode(error);
|
|
174
|
+
if (code === 3) finish({ status: 'no_fill', code });
|
|
175
|
+
else finish({ status: 'error', error: error.message || error.reason || 'load_failed', ...(code === undefined ? {} : { code }) });
|
|
176
|
+
}) as (payload: never) => void));
|
|
177
|
+
try { native.load(); } catch (error) { finish({ status: 'error', error: String(error) }); }
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private requestOptions(adUnit: string, floorKV?: FloorKV): RequestOptions | undefined {
|
|
182
|
+
const kv = this.requestFloor(adUnit, floorKV);
|
|
183
|
+
return kv ? { customTargeting: kv } : undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private requestFloor(adUnit: string, floorKV?: FloorKV): FloorKV | undefined {
|
|
187
|
+
return networkOf(adUnit) === 'gam' && floorKV && Object.keys(floorKV).length > 0 ? { ...floorKV } : undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private handle(format: AdFormat, adUnit: string, floorKV?: FloorKV): AdHandle {
|
|
191
|
+
return { id: `rn-gma-${++this.sequence}`, format, adUnit, ...(floorKV ? { floorKV } : {}) };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AdHandle } from '@gabagool-sdk/core';
|
|
2
|
+
import { bannerFailureStatus, type LoadErrorLike } from './errors.js';
|
|
3
|
+
|
|
4
|
+
export interface BannerFlowPorts {
|
|
5
|
+
/** resolve quando o init da fachada terminou (Banner pode montar antes do init). */
|
|
6
|
+
ready(): Promise<unknown>;
|
|
7
|
+
bannerFor(screen: string, slot: string): Promise<AdHandle | null>;
|
|
8
|
+
markFailed(ad: AdHandle): void;
|
|
9
|
+
trackLoad(unit: string, status: 'fill' | 'no_fill' | 'error'): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BannerFlowHooks {
|
|
13
|
+
setHandle(handle: AdHandle | null): void;
|
|
14
|
+
onLoaded?(): void;
|
|
15
|
+
onFailedToLoad?(error: Error): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Máximo de requests por montagem: primário + `<slot>_fallback` (nunca volta ao primário, como a Dart). */
|
|
19
|
+
export const MAX_BANNER_ATTEMPTS = 2;
|
|
20
|
+
|
|
21
|
+
/** Lógica do Banner sem React: primário -> fallback, banner_load real e nenhuma rejeição solta. */
|
|
22
|
+
export class BannerFlow {
|
|
23
|
+
private generation = 0;
|
|
24
|
+
private attempts = 0;
|
|
25
|
+
private handle: AdHandle | null = null;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly ports: BannerFlowPorts,
|
|
29
|
+
private readonly screen: string,
|
|
30
|
+
private readonly slot: string,
|
|
31
|
+
private readonly hooks: BannerFlowHooks,
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
start(): void {
|
|
35
|
+
this.generation++;
|
|
36
|
+
this.attempts = 0;
|
|
37
|
+
this.request();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
stop(): void { this.generation++; }
|
|
41
|
+
|
|
42
|
+
loaded(): void {
|
|
43
|
+
if (!this.handle) return;
|
|
44
|
+
this.safe(() => this.ports.trackLoad(this.handle!.adUnit, 'fill'));
|
|
45
|
+
this.hooks.onLoaded?.();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
failed(error: Error & LoadErrorLike): void {
|
|
49
|
+
const failed = this.handle;
|
|
50
|
+
if (!failed) return;
|
|
51
|
+
this.safe(() => this.ports.trackLoad(failed.adUnit, bannerFailureStatus(error)));
|
|
52
|
+
this.hooks.onFailedToLoad?.(error);
|
|
53
|
+
this.attempts++;
|
|
54
|
+
if (this.attempts >= MAX_BANNER_ATTEMPTS) { this.set(null); return; }
|
|
55
|
+
this.safe(() => this.ports.markFailed(failed));
|
|
56
|
+
this.request();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private request(): void {
|
|
60
|
+
const generation = this.generation;
|
|
61
|
+
void this.ports.ready()
|
|
62
|
+
.then(() => this.ports.bannerFor(this.screen, this.slot))
|
|
63
|
+
.then((next) => { if (generation === this.generation) this.set(next); })
|
|
64
|
+
.catch(() => { if (generation === this.generation) this.set(null); });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private set(handle: AdHandle | null): void {
|
|
68
|
+
this.handle = handle;
|
|
69
|
+
this.hooks.setHandle(handle);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private safe(fn: () => void): void { try { fn(); } catch { /* telemetria nunca derruba o banner */ } }
|
|
73
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type LoadErrorLike = { code?: string; reason?: string };
|
|
2
|
+
|
|
3
|
+
/** Código numérico do erro de load do Invertase (3 = no fill); undefined se não reconhecido. */
|
|
4
|
+
export const numericCode = (error: LoadErrorLike): number | undefined => {
|
|
5
|
+
if (error.reason === 'no-fill' || error.reason === 'mediation-no-fill') return 3;
|
|
6
|
+
const match = error.code?.match(/(?:^|\/)(\d+)$/);
|
|
7
|
+
return match ? Number(match[1]) : undefined;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Status de `banner_load` para uma falha do componente. */
|
|
11
|
+
export const bannerFailureStatus = (error: LoadErrorLike): 'no_fill' | 'error' =>
|
|
12
|
+
numericCode(error) === 3 ? 'no_fill' : 'error';
|
package/src/facade.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { AdsConsent } from 'react-native-google-mobile-ads';
|
|
2
|
+
import { Dimensions, Platform } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
createGabagool,
|
|
5
|
+
fallbackConfig,
|
|
6
|
+
type AdHandle,
|
|
7
|
+
type AdNetworkAdapter,
|
|
8
|
+
type BootAdResult,
|
|
9
|
+
type FetchLike,
|
|
10
|
+
type Gabagool,
|
|
11
|
+
type GateResult,
|
|
12
|
+
type InterstitialShowResult,
|
|
13
|
+
type KVStorage,
|
|
14
|
+
type RewardedResult,
|
|
15
|
+
type TelemetryTransport,
|
|
16
|
+
type WrapperConfig,
|
|
17
|
+
} from '@gabagool-sdk/core';
|
|
18
|
+
import { ReactNativeGmaAdapter } from './adapter.js';
|
|
19
|
+
import { ReactNativeInstallInfoProvider, type InstallInfoProvider } from './installInfo.js';
|
|
20
|
+
import { AppStateLifecycle } from './lifecycle.js';
|
|
21
|
+
import { AsyncStorageAdapter } from './storage.js';
|
|
22
|
+
import { createReactNativeTransport, reactNativeFetch } from './transport.js';
|
|
23
|
+
|
|
24
|
+
export interface GabagoolInitOptions {
|
|
25
|
+
adapter?: AdNetworkAdapter;
|
|
26
|
+
storage?: KVStorage;
|
|
27
|
+
fetch?: FetchLike;
|
|
28
|
+
transport?: TelemetryTransport;
|
|
29
|
+
requestConsent?: boolean;
|
|
30
|
+
preload?: boolean;
|
|
31
|
+
installInfoProvider?: InstallInfoProvider;
|
|
32
|
+
installReferrer?: string;
|
|
33
|
+
installBeginTimestampSeconds?: number;
|
|
34
|
+
installTimeProvider?: () => Promise<number | null | undefined> | number | null | undefined;
|
|
35
|
+
deepLink?: string;
|
|
36
|
+
override?: Record<string, string>;
|
|
37
|
+
fallback?: WrapperConfig;
|
|
38
|
+
appVersion?: string;
|
|
39
|
+
deviceModel?: string;
|
|
40
|
+
advertisingId?: string;
|
|
41
|
+
debug?: boolean;
|
|
42
|
+
context?: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Fachada singleton equivalente à fachada Dart. */
|
|
46
|
+
export class GabagoolAds {
|
|
47
|
+
private static instance: Gabagool = createGabagool();
|
|
48
|
+
private static lifecycle: AppStateLifecycle | null = null;
|
|
49
|
+
private static gmaAdapter: ReactNativeGmaAdapter | null = null;
|
|
50
|
+
private static readyState = GabagoolAds.newReady();
|
|
51
|
+
|
|
52
|
+
private static newReady(): { done: boolean; promise: Promise<void>; resolve: () => void } {
|
|
53
|
+
let resolve!: () => void;
|
|
54
|
+
const promise = new Promise<void>((r) => { resolve = r; });
|
|
55
|
+
return { done: false, promise, resolve };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Resolve quando o `init` terminou (Banner pode montar antes; ver BannerFlow). Nunca rejeita. */
|
|
59
|
+
static whenReady(): Promise<void> { return this.readyState.promise; }
|
|
60
|
+
|
|
61
|
+
private static resetReady(): void {
|
|
62
|
+
if (this.readyState.done) this.readyState = this.newReady();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
static get core(): Gabagool { return this.instance; }
|
|
66
|
+
|
|
67
|
+
static async init(apiKey: string, baseUrl: string, appId: string, options: GabagoolInitOptions = {}): Promise<void> {
|
|
68
|
+
this.lifecycle?.stop();
|
|
69
|
+
this.instance.dispose();
|
|
70
|
+
this.resetReady();
|
|
71
|
+
this.instance = createGabagool();
|
|
72
|
+
|
|
73
|
+
const adapter = options.adapter ?? new ReactNativeGmaAdapter();
|
|
74
|
+
this.gmaAdapter = adapter instanceof ReactNativeGmaAdapter ? adapter : null;
|
|
75
|
+
if (this.gmaAdapter) await this.gmaAdapter.initialize();
|
|
76
|
+
|
|
77
|
+
if (options.requestConsent) {
|
|
78
|
+
// UMP é deliberadamente assíncrono: falha/formulário não bloqueia o fluxo de anúncios.
|
|
79
|
+
void AdsConsent.requestInfoUpdate()
|
|
80
|
+
.then(() => AdsConsent.loadAndShowConsentFormIfRequired())
|
|
81
|
+
.catch(() => undefined);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const install = await (options.installInfoProvider
|
|
85
|
+
?? new ReactNativeInstallInfoProvider(options.installTimeProvider)).get().catch(() => ({} as import('./installInfo.js').InstallInfo));
|
|
86
|
+
const screen = Dimensions.get('screen');
|
|
87
|
+
const platform = Platform.OS === 'ios' ? 'ios' : 'android';
|
|
88
|
+
const fetchPort = options.fetch ?? reactNativeFetch;
|
|
89
|
+
|
|
90
|
+
await this.instance.init({
|
|
91
|
+
baseUrl,
|
|
92
|
+
apiKey,
|
|
93
|
+
appId,
|
|
94
|
+
adapter,
|
|
95
|
+
platform,
|
|
96
|
+
storage: options.storage ?? new AsyncStorageAdapter(),
|
|
97
|
+
fetch: fetchPort,
|
|
98
|
+
transport: options.transport ?? createReactNativeTransport(baseUrl, apiKey),
|
|
99
|
+
installReferrer: options.installReferrer ?? install.installReferrer,
|
|
100
|
+
installBeginTimestampSeconds: options.installBeginTimestampSeconds ?? install.installBeginTimestampSeconds,
|
|
101
|
+
installTimeProvider: options.installTimeProvider
|
|
102
|
+
?? (install.installTimeMs === undefined ? undefined : () => install.installTimeMs),
|
|
103
|
+
deepLink: options.deepLink ?? install.deepLink,
|
|
104
|
+
...(options.override ? { override: options.override } : {}),
|
|
105
|
+
fallbackConfig: options.fallback ?? fallbackConfig,
|
|
106
|
+
context: {
|
|
107
|
+
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
108
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
109
|
+
screen_width: Math.round(screen.width * screen.scale),
|
|
110
|
+
screen_height: Math.round(screen.height * screen.scale),
|
|
111
|
+
...(options.appVersion ? { app_version: options.appVersion } : {}),
|
|
112
|
+
...(options.deviceModel ? { device_model: options.deviceModel } : {}),
|
|
113
|
+
...(options.advertisingId ? { advertising_id: options.advertisingId } : {}),
|
|
114
|
+
...(options.context ?? {}),
|
|
115
|
+
},
|
|
116
|
+
debug: options.debug === true,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
this.lifecycle = new AppStateLifecycle(this.instance);
|
|
120
|
+
this.lifecycle.start();
|
|
121
|
+
this.readyState.done = true;
|
|
122
|
+
this.readyState.resolve();
|
|
123
|
+
if (options.preload !== false) void this.instance.startAdPreload().catch(() => undefined);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
static showInterstitial(screen?: string): Promise<InterstitialShowResult> { return this.instance.showInterstitial(screen); }
|
|
127
|
+
static showRewarded(screen?: string): Promise<RewardedResult> { return this.instance.showRewarded(screen); }
|
|
128
|
+
static gateUnlock(screen?: string): Promise<GateResult> { return this.instance.showGate(screen); }
|
|
129
|
+
static bannerFor(screen: string, slot = 'banner'): Promise<AdHandle | null> { return this.instance.bannerFor(screen, slot); }
|
|
130
|
+
static showBootAd(): Promise<BootAdResult> { return this.instance.loadAndShowBootAd(); }
|
|
131
|
+
static onAppBackground(): Promise<void> { return this.instance.onBackground(); }
|
|
132
|
+
static onAppForeground(): Promise<boolean> { return this.instance.onForeground(); }
|
|
133
|
+
static onBackground(): Promise<void> { return this.onAppBackground(); }
|
|
134
|
+
static onForeground(): Promise<boolean> { return this.onAppForeground(); }
|
|
135
|
+
static refresh(): Promise<void> { return this.instance.refresh(); }
|
|
136
|
+
static setSegmentOverride(slug: string): Promise<boolean> { return this.instance.setSegmentOverride(slug); }
|
|
137
|
+
static identify(userId: string, traits?: Record<string, unknown>): Promise<void> { return this.instance.identify(userId, traits); }
|
|
138
|
+
static track(name: string, props?: Record<string, unknown>): Promise<void> { return this.instance.track(name, props); }
|
|
139
|
+
|
|
140
|
+
static dispose(): void {
|
|
141
|
+
this.lifecycle?.stop();
|
|
142
|
+
this.lifecycle = null;
|
|
143
|
+
this.instance.dispose();
|
|
144
|
+
this.gmaAdapter = null;
|
|
145
|
+
this.resetReady();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Uso interno do componente Banner para mapear falha real do componente para o fallback do core. */
|
|
149
|
+
static nativeAdapter(): ReactNativeGmaAdapter | null { return this.gmaAdapter; }
|
|
150
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { ReactNativeGmaAdapter } from './adapter.js';
|
|
2
|
+
export { AsyncStorageAdapter } from './storage.js';
|
|
3
|
+
export { reactNativeFetch, createReactNativeTransport } from './transport.js';
|
|
4
|
+
export { AppStateLifecycle, type LifecycleTarget } from './lifecycle.js';
|
|
5
|
+
export { ReactNativeInstallInfoProvider, type InstallInfo, type InstallInfoProvider } from './installInfo.js';
|
|
6
|
+
export { GabagoolAds, type GabagoolInitOptions } from './facade.js';
|
|
7
|
+
export { Banner, type BannerProps } from './Banner.js';
|
|
8
|
+
export * from '@gabagool-sdk/core';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Linking, Platform } from 'react-native';
|
|
2
|
+
import { PlayInstallReferrer } from 'react-native-play-install-referrer';
|
|
3
|
+
|
|
4
|
+
export interface InstallInfo {
|
|
5
|
+
installReferrer?: string;
|
|
6
|
+
installBeginTimestampSeconds?: number;
|
|
7
|
+
installTimeMs?: number;
|
|
8
|
+
deepLink?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface InstallInfoProvider { get(): Promise<InstallInfo>; }
|
|
12
|
+
|
|
13
|
+
/** Provider injetável nos testes/iOS; falhas do módulo nativo viram campos ausentes. */
|
|
14
|
+
export class ReactNativeInstallInfoProvider implements InstallInfoProvider {
|
|
15
|
+
constructor(private readonly installTimeProvider?: () => Promise<number | null | undefined> | number | null | undefined) {}
|
|
16
|
+
|
|
17
|
+
async get(): Promise<InstallInfo> {
|
|
18
|
+
const deepLink = await Linking.getInitialURL().catch(() => null);
|
|
19
|
+
const installTimeMs = await Promise.resolve(this.installTimeProvider?.()).catch(() => null);
|
|
20
|
+
const referrer = Platform.OS === 'android' ? await this.readReferrer() : {};
|
|
21
|
+
return {
|
|
22
|
+
...referrer,
|
|
23
|
+
...(typeof installTimeMs === 'number' && installTimeMs > 0 ? { installTimeMs } : {}),
|
|
24
|
+
...(deepLink ? { deepLink } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private readReferrer(): Promise<Pick<InstallInfo, 'installReferrer' | 'installBeginTimestampSeconds'>> {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
try {
|
|
31
|
+
PlayInstallReferrer.getInstallReferrerInfo((info, error) => {
|
|
32
|
+
if (error || !info) return resolve({});
|
|
33
|
+
resolve({
|
|
34
|
+
...(info.installReferrer ? { installReferrer: info.installReferrer } : {}),
|
|
35
|
+
...(info.installBeginTimestampSeconds > 0
|
|
36
|
+
? { installBeginTimestampSeconds: info.installBeginTimestampSeconds }
|
|
37
|
+
: {}),
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
} catch { resolve({}); }
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AppState, type AppStateStatus, type NativeEventSubscription } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export interface LifecycleTarget {
|
|
4
|
+
onBackground(): Promise<void>;
|
|
5
|
+
onForeground(): Promise<boolean>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Traduz AppState para o lifecycle puro do core e serializa callbacks. */
|
|
9
|
+
export class AppStateLifecycle {
|
|
10
|
+
private state: AppStateStatus = AppState.currentState;
|
|
11
|
+
private subscription: NativeEventSubscription | null = null;
|
|
12
|
+
private pending: Promise<unknown> = Promise.resolve();
|
|
13
|
+
|
|
14
|
+
constructor(private readonly target: LifecycleTarget) {}
|
|
15
|
+
|
|
16
|
+
start(): void {
|
|
17
|
+
if (this.subscription) return;
|
|
18
|
+
this.subscription = AppState.addEventListener('change', this.onChange);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
stop(): void {
|
|
22
|
+
this.subscription?.remove();
|
|
23
|
+
this.subscription = null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private readonly onChange = (next: AppStateStatus): void => {
|
|
27
|
+
const previous = this.state;
|
|
28
|
+
this.state = next;
|
|
29
|
+
if (previous === 'active' && next !== 'active') {
|
|
30
|
+
this.pending = this.pending.then(() => this.target.onBackground()).catch(() => undefined);
|
|
31
|
+
} else if (previous !== 'active' && next === 'active') {
|
|
32
|
+
this.pending = this.pending.then(() => this.target.onForeground()).catch(() => undefined);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import type { KVStorage } from '@gabagool-sdk/core';
|
|
3
|
+
|
|
4
|
+
/** Porta KV concreta; o namespace `@gabagool/` continua sendo aplicado pelo core. */
|
|
5
|
+
export class AsyncStorageAdapter implements KVStorage {
|
|
6
|
+
async get(key: string): Promise<string | null> {
|
|
7
|
+
try { return await AsyncStorage.getItem(key); } catch { return null; }
|
|
8
|
+
}
|
|
9
|
+
async set(key: string, value: string): Promise<void> {
|
|
10
|
+
try { await AsyncStorage.setItem(key, value); } catch { /* persistência nunca derruba o app */ }
|
|
11
|
+
}
|
|
12
|
+
async remove(key: string): Promise<void> {
|
|
13
|
+
try { await AsyncStorage.removeItem(key); } catch { /* persistência nunca derruba o app */ }
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/transport.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { httpTransport, type FetchLike, type TelemetryTransport } from '@gabagool-sdk/core';
|
|
2
|
+
|
|
3
|
+
function allowed(url: string): boolean {
|
|
4
|
+
try {
|
|
5
|
+
const u = new URL(url);
|
|
6
|
+
return u.protocol === 'https:' || (u.protocol === 'http:' && ['localhost', '127.0.0.1', '10.0.2.2'].includes(u.hostname));
|
|
7
|
+
} catch { return false; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Fetch do React Native com a mesma restrição de transporte do porte Dart. */
|
|
11
|
+
export const reactNativeFetch: FetchLike = async (url, init) => {
|
|
12
|
+
if (!allowed(url)) throw new Error('insecure_transport');
|
|
13
|
+
return fetch(url, init) as unknown as ReturnType<FetchLike>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const createReactNativeTransport = (baseUrl: string, apiKey: string): TelemetryTransport =>
|
|
17
|
+
httpTransport({ baseUrl, apiKey, fetch: reactNativeFetch });
|