import type { NativeBridgeWrapperLike, NormalizedRewardedAdsConfig, NormalizedPlatformPlacementConfig, RewardedAdError, RewardedAdProvider, RewardedAdResult, RewardedAdapter, } from './types.ts'; export const NATIVE_REWARDED_REQUEST_EVENT = 'tinivo.rewarded.request'; export const NATIVE_REWARDED_RESPONSE_EVENT = 'tinivo.rewarded.response'; export interface NativeRewardedRuntimeAdapterOptions { platform: NormalizedRewardedAdsConfig['platform']; config: NormalizedRewardedAdsConfig; wrapper?: NativeBridgeWrapperLike | null; timeoutMs?: number; } type NativeRewardedCommand = 'preload' | 'show' | 'reset'; interface NativeRewardedAdapterOptions { provider: RewardedAdProvider; wrapper: NativeBridgeWrapperLike; placements: Record; timeoutMs?: number; } interface NativeRewardedPendingRequest { placement: string; command: Exclude; resolve: (value: RewardedAdResult | void) => void; reject: (reason?: unknown) => void; timer: ReturnType; } interface NativeRewardedResponsePayload { requestId?: string; result?: RewardedAdResult | null; error?: Partial | null; } let nextRequestId = 0; function createError(code: RewardedAdError['code'], message: string, cause?: unknown): RewardedAdError { return { code, message, cause }; } function createErrorResult(error: RewardedAdError): RewardedAdResult { return { completed: false, cancelled: false, earned: false, error, }; } export function createUnsupportedNativeRewardedAdapter(message: string): RewardedAdapter { const states = new Map infer T ? T : never>(); return { async init() { return undefined; }, async preload(placement: string) { states.set(placement, 'unsupported'); throw createError('unsupported', message); }, async show(placement: string) { states.set(placement, 'unsupported'); return createErrorResult(createError('unsupported', message)); }, isReady() { return false; }, getState(placement: string) { return states.get(placement) ?? 'unsupported'; }, reset(placement?: string) { if (placement) { states.delete(placement); return; } states.clear(); }, }; } export function resolveNativeBridgeWrapper(override?: NativeBridgeWrapperLike | null) { if (override) { return override; } const globalScope = globalThis as typeof globalThis & { native?: { jsbBridgeWrapper?: NativeBridgeWrapperLike; }; cc?: { native?: { jsbBridgeWrapper?: NativeBridgeWrapperLike; }; }; jsb?: { jsbBridgeWrapper?: NativeBridgeWrapperLike; }; jsbBridgeWrapper?: NativeBridgeWrapperLike; }; return globalScope.native?.jsbBridgeWrapper ?? globalScope.cc?.native?.jsbBridgeWrapper ?? globalScope.jsb?.jsbBridgeWrapper ?? globalScope.jsbBridgeWrapper ?? null; } function safeParseJson(payload: string): NativeRewardedResponsePayload | null { try { const parsed = JSON.parse(payload) as NativeRewardedResponsePayload; return parsed && typeof parsed === 'object' ? parsed : null; } catch { return null; } } export function createNativeRewardedAdapter(options: NativeRewardedAdapterOptions): RewardedAdapter { const timeoutMs = Math.max(1, options.timeoutMs ?? 15000); const readyPlacements = new Set(); const states = new Map infer T ? T : never>(); const pendingRequests = new Map(); const pendingPreloads = new Map>(); const handleResponse = (payload: string) => { const parsed = safeParseJson(payload); if (!parsed?.requestId) { return; } const pending = pendingRequests.get(parsed.requestId); if (!pending) { return; } clearTimeout(pending.timer); pendingRequests.delete(parsed.requestId); if (pending.command === 'preload') { pendingPreloads.delete(pending.placement); if (parsed.error) { states.set(pending.placement, 'failed'); pending.reject(createError(parsed.error.code ?? 'loadFailed', parsed.error.message ?? 'native preload failed', parsed.error)); return; } readyPlacements.add(pending.placement); states.set(pending.placement, 'ready'); pending.resolve(); return; } if (parsed.error) { states.set(pending.placement, 'failed'); pending.resolve(createErrorResult(createError(parsed.error.code ?? 'showFailed', parsed.error.message ?? 'native show failed', parsed.error))); return; } readyPlacements.delete(pending.placement); states.set(pending.placement, 'idle'); pending.resolve(parsed.result ?? createErrorResult(createError('unknown', 'native rewarded result is invalid', parsed))); }; options.wrapper.addNativeEventListener(NATIVE_REWARDED_RESPONSE_EVENT, handleResponse); const dispatch = (placement: string, command: Exclude) => { if (command === 'preload') { const existingPreload = pendingPreloads.get(placement); if (existingPreload) { return existingPreload as Promise; } if (readyPlacements.has(placement)) { states.set(placement, 'ready'); return Promise.resolve(undefined as T); } } states.set(placement, command === 'preload' ? 'loading' : 'showing'); const requestPromise = new Promise((resolve, reject) => { nextRequestId += 1; const requestId = `native-rewarded:${command}:${placement}:${nextRequestId}`; const timer = setTimeout(() => { pendingRequests.delete(requestId); if (command === 'preload') { pendingPreloads.delete(placement); } states.set(placement, 'failed'); const error = createError(command === 'preload' ? 'loadFailed' : 'timeout', `${placement} ${command} timed out`); if (command === 'preload') { reject(error); return; } resolve(createErrorResult(error) as T); }, timeoutMs); pendingRequests.set(requestId, { placement, command, resolve: resolve as NativeRewardedPendingRequest['resolve'], reject, timer, }); options.wrapper.dispatchEventToNative(NATIVE_REWARDED_REQUEST_EVENT, JSON.stringify({ requestId, command, provider: options.provider, placement, config: options.placements[placement], })); }); if (command === 'preload') { pendingPreloads.set(placement, requestPromise as Promise); } return requestPromise; }; return { async init() { return undefined; }, preload(placement: string) { return dispatch(placement, 'preload'); }, show(placement: string) { return dispatch(placement, 'show'); }, isReady(placement: string) { return readyPlacements.has(placement); }, getState(placement: string) { return states.get(placement) ?? 'idle'; }, reset(placement?: string) { if (placement) { readyPlacements.delete(placement); states.set(placement, 'idle'); pendingPreloads.delete(placement); } else { readyPlacements.clear(); states.clear(); pendingPreloads.clear(); } options.wrapper.dispatchEventToNative(NATIVE_REWARDED_REQUEST_EVENT, JSON.stringify({ command: 'reset', provider: options.provider, ...(placement ? { placement } : {}), })); }, }; }