Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
325 changes: 205 additions & 120 deletions examples/adsTester/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import 'react-native-get-random-values';
import React, {useCallback, useEffect, useState} from 'react';
import {
Platform,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
Expand All @@ -13,19 +14,31 @@ import Purchases, {
} from 'react-native-purchases';
import mobileAds, {
AdEventType,
BannerAd,
BannerAdSize,
RewardedAdEventType,
RewardedInterstitialAd,
TestIds,
} from 'react-native-google-mobile-ads';
import {v4 as uuidv4} from 'uuid';
import {
RevenueCatAdMob,
TrackableAd,
useTrackingBannerAdProps,
} from 'react-native-purchases-admob';

// Your RevenueCat public SDK key (a Test Store key works while developing).
/**
* Configuration. Everything ships with placeholder / Google test values that
* are safe to commit. For a real reward grant, set your RevenueCat key, an
* AdMob unit whose server-side verification URL points at RevenueCat, and your
* AdMob app id (`Info.plist` + `app.json`).
*/
const API_KEY = 'YOUR_REVENUECAT_API_KEY';

// Google's official test rewarded-interstitial ad unit (per-platform) — safe
// to commit and always fills. Swap for your own AdMob unit (with its
// server-side verification URL pointed at RevenueCat) to grant a real reward.
const AD_UNIT_ID = TestIds.REWARDED_INTERSTITIAL;
const AD_UNITS = {
interstitial: TestIds.INTERSTITIAL,
rewarded: TestIds.REWARDED,
rewardedInterstitial: TestIds.REWARDED_INTERSTITIAL,
appOpen: TestIds.APP_OPEN,
banner: TestIds.BANNER,
};

function describeReward(reward: VerifiedReward): string {
switch (reward.type) {
Expand All @@ -40,145 +53,217 @@ function describeReward(reward: VerifiedReward): string {
}
}

export default function App() {
const [status, setStatus] = useState('Configuring…');
const [impressionId, setImpressionId] = useState<string | null>(null);
const [result, setResult] = useState<string | null>(null);
const [ready, setReady] = useState(false);
function describeResult(result: RewardVerificationResult): string {
if (result.failed || !result.reward) {
return '❌ verification failed';
}
const extra =
result.moreRewards.length > 0 ? ` (+${result.moreRewards.length} more)` : '';
return `✅ ${describeReward(result.reward)}${extra}`;
}

useEffect(() => {
(async () => {
Purchases.setLogLevel(Purchases.LOG_LEVEL.DEBUG);
Purchases.configure({apiKey: API_KEY});
await mobileAds().initialize();
setStatus('Ready. Tap to load a rewarded ad.');
setReady(true);
})();
}, []);
type FullScreenAd = TrackableAd;

const loadAndShow = useCallback(async () => {
setReady(false);
setResult(null);
setStatus('Generating verification token…');
interface FormatProps {
title: string;
create: () => Promise<FullScreenAd> | FullScreenAd;
show: (ad: FullScreenAd, onResult: (text: string) => void) => Promise<void>;
}

// react-native-google-mobile-ads doesn't expose AdMob's response id before
// the ad loads (SSV options must be set at request time), so assign your own
// unique impression ID. Reuse it for your RevenueCat ad-tracking calls to
// correlate the reward with the impression.
const id = uuidv4();
setImpressionId(id);
/** One load/show card per full-screen format. */
function FormatCard({title, create, show}: FormatProps) {
const [ad, setAd] = useState<FullScreenAd | null>(null);
const [status, setStatus] = useState('No ad loaded');
const [result, setResult] = useState<string | null>(null);
const [busy, setBusy] = useState(false);

let token;
const load = useCallback(async () => {
setBusy(true);
setResult(null);
setStatus('Loading…');
let created: FullScreenAd;
try {
token = await Purchases.generateRewardVerificationToken(id);
created = await create();
} catch (e) {
setStatus(`❌ Failed to generate verification token: ${e}`);
setReady(true);
setBusy(false);
setStatus(`Error: ${(e as Error).message}`);
return;
}

// Forward the token to AdMob's server-side verification options.
const ad = RewardedInterstitialAd.createForAdRequest(AD_UNIT_ID, {
serverSideVerificationOptions: {
userId: token.appUserID,
customData: token.customData,
},
created.addAdEventsListener(({type, payload}) => {
switch (type) {
case AdEventType.LOADED:
case RewardedAdEventType.LOADED:
setAd(created);
setBusy(false);
setStatus(`Ad ready (${created.responseInfo?.responseId ?? '?'})`);
break;
case AdEventType.IMPRESSION:
setStatus('Impression recorded');
break;
case AdEventType.CLOSED:
setAd(null);
setBusy(false);
setStatus('Dismissed');
break;
case AdEventType.ERROR:
setAd(null);
setBusy(false);
setStatus(`Error: ${(payload as Error).message}`);
break;
}
});
created.load();
Comment thread
cursor[bot] marked this conversation as resolved.
}, [create]);

const unsubLoaded = ad.addAdEventListener(
RewardedAdEventType.LOADED,
() => {
setStatus('Ad loaded. Showing…');
ad.show();
},
);

let finished = false;

const cleanup = () => {
unsubLoaded();
unsubEarned();
unsubError();
unsubClosed();
};

// When the user earns the reward, poll RevenueCat for the verified result.
const unsubEarned = ad.addAdEventListener(
RewardedAdEventType.EARNED_REWARD,
async () => {
setStatus('Reward earned. Verifying…');
try {
const res: RewardVerificationResult =
await Purchases.pollRewardVerification(token.clientTransactionId);
if (res.failed || !res.reward) {
setResult('❌ verification failed');
} else {
const extra =
res.moreRewards.length > 0
? ` (+${res.moreRewards.length} more)`
: '';
setResult(`✅ ${describeReward(res.reward)}${extra}`);
}
const onPress = useCallback(async () => {
if (ad) {
setBusy(true);
try {
await show(ad, text => {
setStatus('Done');
} finally {
finished = true;
setReady(true);
}
},
);
setResult(text);
});
} catch (e) {
// show() rejects without an ERROR event when the ad is not loaded.
setAd(null);
setBusy(false);
setStatus(`Error: ${(e as Error).message}`);
}
} else {
await load();
}
}, [ad, load, show]);
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

const unsubError = ad.addAdEventListener(AdEventType.ERROR, error => {
setStatus(`❌ Ad error: ${error.message}`);
finished = true;
setReady(true);
cleanup();
});
return (
<View style={styles.card}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.status}>{status}</Text>
{result != null && <Text style={styles.result}>{result}</Text>}
<TouchableOpacity
style={[styles.button, busy && styles.buttonDisabled]}
disabled={busy}
onPress={onPress}>
<Text style={styles.buttonText}>{ad ? `Show ${title}` : `Load ${title}`}</Text>
</TouchableOpacity>
</View>
);
}

const unsubClosed = ad.addAdEventListener(AdEventType.CLOSED, () => {
if (!finished) {
finished = true;
setReady(true);
}
cleanup();
});
const showPlain = (ad: FullScreenAd) =>
RevenueCatAdMob.showAndTrack(ad, {placement: 'shown_from_card'});

const showVerified = (
ad: FullScreenAd,
onResult: (text: string) => void,
) =>
RevenueCatAdMob.showWithRewardVerification(ad, {
placement: 'shown_from_card',
onRewardVerificationStarted: () => onResult('Verifying reward…'),
onRewardVerificationCompleted: result => onResult(describeResult(result)),
});

setStatus('Loading ad…');
ad.load();
export default function App() {
const [configured, setConfigured] = useState(false);
const [bannerStatus, setBannerStatus] = useState('Banner not loaded');

useEffect(() => {
(async () => {
Purchases.setLogLevel(Purchases.LOG_LEVEL.DEBUG);
Purchases.configure({apiKey: API_KEY});
await mobileAds().initialize();
setConfigured(true);
})();
}, []);

const bannerProps = useTrackingBannerAdProps({
unitId: AD_UNITS.banner,
size: BannerAdSize.BANNER,
placement: 'banner_bottom',
onAdLoaded: ({responseInfo}) =>
setBannerStatus(`Banner loaded (${responseInfo?.responseId ?? '?'})`),
onPaid: paid => setBannerStatus(`Banner paid: ${paid.value} ${paid.currency}`),
});

if (!configured) {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.status}>Configuring…</Text>
</SafeAreaView>
);
}

return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<Text style={styles.title}>Rewarded Ad Verification</Text>
<Text style={styles.status}>{status}</Text>
{impressionId != null && (
<Text style={styles.impression}>impressionId: {impressionId}</Text>
)}
{result != null && <Text style={styles.result}>{result}</Text>}
<TouchableOpacity
style={[styles.button, !ready && styles.buttonDisabled]}
disabled={!ready}
onPress={loadAndShow}>
<Text style={styles.buttonText}>Load & show rewarded ad</Text>
</TouchableOpacity>
<ScrollView contentContainerStyle={styles.content}>
<Text style={styles.header}>RevenueCat AdMob adapter ({Platform.OS})</Text>
<FormatCard
title="Interstitial"
create={() =>
RevenueCatAdMob.createInterstitialAd(AD_UNITS.interstitial, {
placement: 'interstitial_card',
})
}
show={showPlain}
/>
<FormatCard
title="Rewarded"
create={() =>
RevenueCatAdMob.createRewardedAd(AD_UNITS.rewarded, {
placement: 'rewarded_card',
enableRewardVerification: true,
})
}
show={showVerified}
/>
<FormatCard
title="Rewarded Interstitial"
create={() =>
RevenueCatAdMob.createRewardedInterstitialAd(
AD_UNITS.rewardedInterstitial,
{placement: 'rewarded_interstitial_card', enableRewardVerification: true},
)
}
show={showVerified}
/>
<FormatCard
title="App Open"
create={() =>
RevenueCatAdMob.createAppOpenAd(AD_UNITS.appOpen, {
placement: 'app_open_card',
})
}
show={showPlain}
/>
<Text style={styles.status}>{bannerStatus}</Text>
</ScrollView>
<View style={styles.banner}>
<BannerAd {...bannerProps} />
</View>
</SafeAreaView>
);
}

const styles = StyleSheet.create({
container: {flex: 1, backgroundColor: '#fff'},
content: {flex: 1, justifyContent: 'center', padding: 24, gap: 16},
title: {fontSize: 22, fontWeight: '600', textAlign: 'center'},
status: {fontSize: 16, textAlign: 'center', color: '#333'},
impression: {fontSize: 13, textAlign: 'center', color: '#888'},
result: {fontSize: 18, textAlign: 'center', fontWeight: '600'},
content: {padding: 16, gap: 12},
header: {fontSize: 20, fontWeight: '600', textAlign: 'center', marginBottom: 8},
card: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 12,
padding: 12,
gap: 8,
},
title: {fontSize: 16, fontWeight: '600'},
status: {fontSize: 14, color: '#333', textAlign: 'center'},
result: {fontSize: 16, fontWeight: '600', textAlign: 'center'},
button: {
backgroundColor: '#f2545b',
paddingVertical: 16,
borderRadius: 12,
paddingVertical: 12,
borderRadius: 10,
alignItems: 'center',
},
buttonDisabled: {opacity: 0.4},
buttonText: {color: '#fff', fontSize: 16, fontWeight: '600'},
buttonText: {color: '#fff', fontSize: 15, fontWeight: '600'},
banner: {alignItems: 'center', paddingVertical: 8},
});
4 changes: 4 additions & 0 deletions examples/adsTester/babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ module.exports = {
extensions: ['.tsx', '.ts', '.js', '.json'],
alias: {
[pak.name]: path.join(__dirname, '../..', pak.source),
'react-native-purchases-admob': path.join(
__dirname,
'../../react-native-purchases-admob/src/index',
),
},
},
],
Expand Down
Loading