import { ConfigValidationError } from '../../core/src/config.ts'; import type { AdMobPlacementConfig, MaxPlacementConfig, RewardedAdsConfig, RewardedAdTargetPlatform, TopOnPlacementConfig, } from '../../core/src/types.ts'; import { createNativeProviderInjectionPlan, resolveBuildPlatformSelection } from './platform-select.ts'; export interface ResolvedNativeProviderRuntimeConfig { targetPlatform: 'android' | 'ios'; provider: 'admob' | 'max' | 'topon'; shared: { sdkKey?: string; appId?: string; appKey?: string; }; placements: Record; } export interface NativeProviderArtifact { outputPath: string; content: string; } function renderNativePatchPlan(config: ResolvedNativeProviderRuntimeConfig) { if (config.targetPlatform === 'android') { const actions = [ { file: 'native/engine/android/app/build.gradle', operation: 'apply-gradle-fragment', snippetFile: 'native/engine/android/rewarded-ads-build.gradle.snippet', }, { file: 'native/engine/android/app/src/**/AppActivity.java|kt', operation: 'install-rewarded-ads', snippetFile: 'native/engine/android/rewarded-ads-app-activity.snippet.kt', }, ]; if (config.provider === 'admob') { actions.push({ file: 'native/engine/android/app/AndroidManifest.xml', operation: 'inject-admob-application-id', snippetFile: 'ads.config.json', }); actions.push({ file: 'proj/gradle.properties', operation: 'ensure-min-sdk-version', snippetFile: 'ads.config.json', }); } return { platform: 'android', provider: config.provider, actions, }; } return { platform: 'ios', provider: config.provider, actions: [ { file: 'native/engine/ios/AppDelegate.mm', operation: 'install-rewarded-ads', snippetFile: 'native/engine/ios/rewarded-ads-appdelegate.snippet.mm', }, { file: 'native/engine/ios/CMakeLists.txt', operation: 'include-generated-sources', snippetFile: 'native/engine/ios/rewarded-ads-patch-plan.json', }, ], }; } function getNativeBridgeClassName(config: ResolvedNativeProviderRuntimeConfig) { if (config.provider === 'admob') { return 'RewardedAdsAdMobBridge'; } if (config.provider === 'max') { return 'RewardedAdsMaxBridge'; } return 'RewardedAdsTopOnBridge'; } function renderAndroidInstallerSource(config: ResolvedNativeProviderRuntimeConfig) { const bridgeClassName = getNativeBridgeClassName(config); const privacyBridgeProperty = config.provider === 'admob' ? ' private var privacyBridge: PrivacyConsentBridge? = null\n' : ''; const installPrivacyBridge = config.provider === 'admob' ? ` privacyBridge = PrivacyConsentBridge(activity).also { current -> current.install() } ` : ''; const resetPrivacyBridge = config.provider === 'admob' ? ' privacyBridge = null\n' : ''; return `package com.tinivo.rewardedads import android.app.Activity object RewardedAdsInstaller { private var installed = false private var bridge: ${bridgeClassName}? = null ${privacyBridgeProperty} @JvmStatic fun install(activity: Activity) { if (installed) { return } installed = true ${installPrivacyBridge} bridge = ${bridgeClassName}(activity).also { current -> current.install() } } @JvmStatic fun reset() { installed = false bridge = null ${resetPrivacyBridge} } } `; } function renderIosInstallerSource(config: ResolvedNativeProviderRuntimeConfig) { const bridgeClassName = getNativeBridgeClassName(config); return `#import "RewardedAdsInstaller.h" #import "${bridgeClassName}.h" static ${bridgeClassName} *gTinivoRewardedAdsBridge = nil; void TinivoInstallRewardedAds(UIViewController *rootViewController) { if (gTinivoRewardedAdsBridge != nil) { return; } gTinivoRewardedAdsBridge = [[${bridgeClassName} alloc] init]; [gTinivoRewardedAdsBridge installWithRootViewController:rootViewController]; } void TinivoResetRewardedAds(void) { gTinivoRewardedAdsBridge = nil; } `; } function renderIosInstallerHeaderSource() { return `#import FOUNDATION_EXPORT void TinivoInstallRewardedAds(UIViewController *rootViewController); FOUNDATION_EXPORT void TinivoResetRewardedAds(void); `; } function renderAndroidActivitySnippetSource() { return `import com.tinivo.rewardedads.RewardedAdsInstaller override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) RewardedAdsInstaller.install(this) } `; } function renderAndroidGradleSnippetSource() { return `// native/engine/android/app/build.gradle apply from: "../rewarded-ads-dependencies.gradle" `; } function renderAndroidDependenciesGradleSource(gradleDependencies: string[]) { return `dependencies { ${gradleDependencies.map((dependency) => ` ${dependency}`).join('\n')} } `; } function renderIosAppDelegateSnippetSource() { return `// native/engine/ios/AppDelegate.mm #import "RewardedAdsInstaller.h" - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { TinivoInstallRewardedAds(self.window.rootViewController); return YES; } `; } function assertNativeTargetPlatform(platform: RewardedAdTargetPlatform): 'android' | 'ios' { if (platform !== 'android' && platform !== 'ios') { throw new ConfigValidationError(`Native provider config only supports android/ios, got ${platform}`); } return platform; } export function resolveNativeProviderRuntimeConfig(options: { targetPlatform: RewardedAdTargetPlatform; config: RewardedAdsConfig; }): ResolvedNativeProviderRuntimeConfig { const targetPlatform = assertNativeTargetPlatform(options.targetPlatform); const selection = resolveBuildPlatformSelection({ targetPlatform, config: options.config, }); if (selection.provider !== 'admob' && selection.provider !== 'max' && selection.provider !== 'topon') { throw new ConfigValidationError(`Native provider config only supports admob/max/topon, got ${selection.provider}`); } const placements: Record = {}; let shared: ResolvedNativeProviderRuntimeConfig['shared'] | null = null; for (const [placement, placementConfig] of Object.entries(options.config.placements)) { const current = placementConfig?.[targetPlatform]; if (!current) { continue; } if (current.provider !== selection.provider) { throw new ConfigValidationError( `Target platform ${targetPlatform} must use shared provider credentials for a single rewarded provider`, ); } placements[placement] = { adUnitId: current.adUnitId, }; if (selection.provider === 'admob') { const admobConfig = current as AdMobPlacementConfig; if (!shared) { shared = { appId: admobConfig.appId }; continue; } if (shared.appId !== admobConfig.appId) { throw new ConfigValidationError( `Target platform ${targetPlatform} must use shared provider credentials for AdMob appId`, ); } continue; } if (selection.provider === 'max') { const maxConfig = current as MaxPlacementConfig; if (!shared) { shared = { sdkKey: maxConfig.sdkKey }; continue; } if (shared.sdkKey !== maxConfig.sdkKey) { throw new ConfigValidationError( `Target platform ${targetPlatform} must use shared provider credentials for MAX sdkKey`, ); } continue; } const toponConfig = current as TopOnPlacementConfig; if (!shared) { shared = { appId: toponConfig.appId, appKey: toponConfig.appKey, }; continue; } if (shared.appId !== toponConfig.appId || shared.appKey !== toponConfig.appKey) { throw new ConfigValidationError( `Target platform ${targetPlatform} must use shared provider credentials for TopOn appId/appKey`, ); } } if (!shared) { throw new ConfigValidationError(`No native provider placements configured for ${targetPlatform}`); } return { targetPlatform, provider: selection.provider, shared, placements, }; } export function renderNativeProviderArtifacts( config: ResolvedNativeProviderRuntimeConfig, ): NativeProviderArtifact[] { const injectionPlan = createNativeProviderInjectionPlan({ targetPlatform: config.targetPlatform, provider: config.provider, }); const artifacts: NativeProviderArtifact[] = []; const providerConfigOutputPath = config.targetPlatform === 'android' ? 'data/rewarded-provider-config.json' : 'native/engine/ios/rewarded-provider-config.json'; artifacts.push({ outputPath: providerConfigOutputPath, content: `${JSON.stringify({ provider: config.provider, platform: config.targetPlatform, shared: config.shared, placements: config.placements, }, null, 2)}\n`, }); if (config.targetPlatform === 'android') { artifacts.push({ outputPath: 'native/engine/android/rewarded-ads-dependencies.gradle', content: renderAndroidDependenciesGradleSource(injectionPlan.gradleDependencies), }); artifacts.push({ outputPath: 'native/engine/android/app/src/com/tinivo/rewardedads/RewardedAdsInstaller.kt', content: renderAndroidInstallerSource(config), }); artifacts.push({ outputPath: 'native/engine/android/rewarded-ads-app-activity.snippet.kt', content: renderAndroidActivitySnippetSource(), }); artifacts.push({ outputPath: 'native/engine/android/rewarded-ads-build.gradle.snippet', content: renderAndroidGradleSnippetSource(), }); artifacts.push({ outputPath: 'native/engine/android/rewarded-ads-patch-plan.json', content: `${JSON.stringify(renderNativePatchPlan(config), null, 2)}\n`, }); } if (config.targetPlatform === 'ios') { if (injectionPlan.podDependencies.length > 0) { artifacts.push({ outputPath: 'native/engine/ios/rewarded-ads-dependencies.podfile.fragment', content: `${injectionPlan.podDependencies.join('\n')}\n`, }); } artifacts.push({ outputPath: 'native/engine/ios/RewardedAdsInstaller.h', content: renderIosInstallerHeaderSource(), }); artifacts.push({ outputPath: 'native/engine/ios/RewardedAdsInstaller.mm', content: renderIosInstallerSource(config), }); artifacts.push({ outputPath: 'native/engine/ios/rewarded-ads-appdelegate.snippet.mm', content: renderIosAppDelegateSnippetSource(), }); artifacts.push({ outputPath: 'native/engine/ios/rewarded-ads-patch-plan.json', content: `${JSON.stringify(renderNativePatchPlan(config), null, 2)}\n`, }); } return artifacts; }