React & React NativeUpdated Jun 2
Step 2: The useRewardedAd hook
A reusable hook is the cleanest way to use AppLixir in React. It loads the SDK
once, exposes a ready flag, and turns the callback API into a promise that
resolves true only when the user watched the ad through to complete.
The hook
// useRewardedAd.js
import { useCallback, useEffect, useRef, useState } from "react";
const SDK_SRC = "https://cdn.applixir.com/applixir.app.v6.1.0.js";
function loadSdk() {
if (typeof window !== "undefined" && window.initializeAndOpenPlayer) {
return Promise.resolve();
}
return new Promise((resolve, reject) => {
const existing = document.querySelector(`script[src="${SDK_SRC}"]`);
if (existing) {
existing.addEventListener("load", resolve);
existing.addEventListener("error", reject);
return;
}
const s = document.createElement("script");
s.src = SDK_SRC;
s.async = true;
s.onload = resolve;
s.onerror = reject;
document.head.appendChild(s);
});
}
export function useRewardedAd({ apiKey, injectionElementId = "applixir-ad-container" }) {
const [ready, setReady] = useState(false);
const settle = useRef(null);
useEffect(() => {
let alive = true;
loadSdk().then(() => alive && setReady(true)).catch(console.error);
return () => { alive = false; };
}, []);
const finish = (value) => {
if (settle.current) { settle.current(value); settle.current = null; }
};
const showAd = useCallback(() => {
if (!ready || !window.initializeAndOpenPlayer) return Promise.resolve(false);
return new Promise((resolve) => {
settle.current = resolve;
window.initializeAndOpenPlayer({
apiKey,
injectionElementId,
adStatusCallbackFn: (status) => {
// "complete" = watched through → optimistic true.
if (status?.type === "complete") finish(true);
// terminal no-reward states → false
else if (["skipped", "manuallyEnded", "allAdsCompleted", "consentDeclined"]
.includes(status?.type)) finish(false);
},
adErrorCallbackFn: (error) => {
console.error("AppLixir error:", error.getError().data);
finish(false);
},
});
});
}, [ready, apiKey, injectionElementId]);
return { ready, showAd };
}
How showAd() resolves
| Resolves | When |
|---|---|
true | status.type === "complete" — the user watched the full ad |
false | skipped, manuallyEnded, allAdsCompleted (no ad / dismissed), consentDeclined, or any error |
allAdsCompleted fires both when the response had no ads and after a real
completion — the settle ref guarantees the promise resolves exactly once, so a
trailing allAdsCompleted after complete is a harmless no-op.
Using it in a component
import { useRewardedAd } from "./useRewardedAd";
export function WatchAdButton({ onReward }) {
const { ready, showAd } = useRewardedAd({ apiKey: "YOUR-API-KEY" });
const handleClick = async () => {
const watched = await showAd();
if (watched) {
onReward?.(); // optimistic UI: "reward incoming…"
// Grant the persistent reward from your server (see Step 5).
}
};
return (
<>
<div id="applixir-ad-container" />
<button disabled={!ready} onClick={handleClick}>
{ready ? "Watch ad" : "Loading…"}
</button>
</>
);
}
TypeScript
// useRewardedAd.ts
import { useCallback, useEffect, useRef, useState } from "react";
type AdStatus = { type: string; ad?: unknown };
type AdError = { getError: () => { data: { type: string; errorCode: number; errorMessage: string } } };
interface ApplixirOptions {
apiKey: string;
injectionElementId: string;
adStatusCallbackFn: (status: AdStatus) => void;
adErrorCallbackFn?: (error: AdError) => void;
}
declare global {
interface Window {
initializeAndOpenPlayer?: (options: ApplixirOptions) => void;
}
}
const SDK_SRC = "https://cdn.applixir.com/applixir.app.v6.1.0.js";
function loadSdk(): Promise<void> {
if (typeof window !== "undefined" && window.initializeAndOpenPlayer) return Promise.resolve();
return new Promise((resolve, reject) => {
const existing = document.querySelector<HTMLScriptElement>(`script[src="${SDK_SRC}"]`);
if (existing) {
existing.addEventListener("load", () => resolve());
existing.addEventListener("error", reject);
return;
}
const s = document.createElement("script");
s.src = SDK_SRC;
s.async = true;
s.onload = () => resolve();
s.onerror = reject;
document.head.appendChild(s);
});
}
export function useRewardedAd(opts: { apiKey: string; injectionElementId?: string }) {
const { apiKey, injectionElementId = "applixir-ad-container" } = opts;
const [ready, setReady] = useState(false);
const settle = useRef<((v: boolean) => void) | null>(null);
useEffect(() => {
let alive = true;
loadSdk().then(() => alive && setReady(true)).catch(console.error);
return () => { alive = false; };
}, []);
const finish = (value: boolean) => {
if (settle.current) { settle.current(value); settle.current = null; }
};
const showAd = useCallback((): Promise<boolean> => {
if (!ready || !window.initializeAndOpenPlayer) return Promise.resolve(false);
return new Promise<boolean>((resolve) => {
settle.current = resolve;
window.initializeAndOpenPlayer!({
apiKey,
injectionElementId,
adStatusCallbackFn: (status) => {
if (status?.type === "complete") finish(true);
else if (["skipped", "manuallyEnded", "allAdsCompleted", "consentDeclined"].includes(status?.type)) finish(false);
},
adErrorCallbackFn: (error) => {
console.error("AppLixir error:", error.getError().data);
finish(false);
},
});
});
}, [ready, apiKey, injectionElementId]);
return { ready, showAd };
}
React notes
- Strict Mode double-invokes effects in development. The hook checks the DOM
and the global before injecting, so you never get a duplicate
<script>. - One anchor, kept mounted. Don't unmount the
injectionElementIddiv while an ad is open — render it at a stable place in your tree. - Server-side rendering is covered in Step 3 (Next.js): the SDK is browser-only and must load in a client component.
Next: the Framework recipes for Vite, Next.js, and CRA.