mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-09 22:35:56 +05:45
* feat(ads): dispatch checkpoint ad presentation by format Ad units are format-locked, so the SDK needs to know which AdMob loader a checkpoint ad step's unit belongs to. The backend now requires ad_format on the step; core surfaces it on ResolvedAdStep, RevenueCatUI threads it through AdPresentationParams, and the AdMob adapter replaces its three per-format presenters with one AdMobPresenter that dispatches on it and fails fast on formats it does not implement. * refactor: presenter composition * fix: use renamed InviteOnlyCheckpointsApi SPI * test(ads): assert on shown and failed ad executions The ad outcome classes are gone, so the format-aware presenter reports shown for every format and the tests match the execution cases. * test(ads): match failed ad executions and the ad format param Claude-Session: https://claude.ai/code/session_01Gj5NLpaCp45dfxDVR5rScP * fix(ads): keep the ad params initializer internal-only The adapter tests are the only external caller, so the init is SPI rather than public, as the no_new_public_inits rule requires. Also passes the ad format in the missing-presenter behavior test. Claude-Session: https://claude.ai/code/session_01Gj5NLpaCp45dfxDVR5rScP * fix(ads): mark AdMob presenter inits as SPI for lint Claude-Session: https://claude.ai/code/session_01Gj5NLpaCp45dfxDVR5rScP * test(api): cover AdPresentationParams.adFormat in the RevenueCatUI API tester
78 lines
3.2 KiB
Swift
78 lines
3.2 KiB
Swift
//
|
|
// Copyright RevenueCat Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the MIT License (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://opensource.org/licenses/MIT
|
|
//
|
|
// DefaultCheckpointWorkflowResolver+Ad.swift
|
|
//
|
|
// Created by RevenueCat.
|
|
|
|
import Foundation
|
|
|
|
extension DefaultCheckpointWorkflowResolver {
|
|
|
|
static let adStepType = "ad"
|
|
static let adIdentifierParam = "ad_identifier"
|
|
static let mediatorParam = "mediator"
|
|
static let adFormatParam = "ad_format"
|
|
|
|
func resolveAdStep(
|
|
_ rule: CheckpointRule,
|
|
step: WorkflowStep,
|
|
in workflowData: WorkflowDataResult,
|
|
traceID: String
|
|
) async -> (resolution: CheckpointResolution, stepEvents: [WorkflowEvent]) {
|
|
guard workflowData.workflow.steps.count == 1 else {
|
|
return (Self.unservable(rule, reason: "an ad step cannot be mixed with other steps"), [])
|
|
}
|
|
|
|
return await Self.trackingStepEvents(for: step, in: workflowData, traceID: traceID) {
|
|
Self.resolveAd(rule, step: step)
|
|
}
|
|
}
|
|
|
|
/// Serves a workflow whose only step is a terminal `ad` step as an ad the app owns.
|
|
///
|
|
/// Only `ad_identifier`, `mediator` and `ad_format` are validated. Anything else the step happens to carry
|
|
/// is ignored rather than treated as unservable, since a step of this kind renders nothing.
|
|
static func resolveAd(_ rule: CheckpointRule, step: WorkflowStep) -> CheckpointResolution {
|
|
guard let adIdentifier = Self.stringParam(Self.adIdentifierParam, in: step), !adIdentifier.isEmpty else {
|
|
return Self.unservable(rule, reason: "the ad step has no valid ad identifier")
|
|
}
|
|
guard let mediator = Self.stringParam(Self.mediatorParam, in: step), !mediator.isEmpty else {
|
|
return Self.unservable(rule, reason: "the ad step has no valid mediator")
|
|
}
|
|
guard let adFormat = Self.stringParam(Self.adFormatParam, in: step), !adFormat.isEmpty else {
|
|
return Self.unservable(rule, reason: "the ad step has no valid ad format")
|
|
}
|
|
|
|
return .matchedAd(ResolvedAdStep(
|
|
adIdentifier: adIdentifier,
|
|
mediator: Self.normalizedMediator(mediator),
|
|
adFormat: AdFormat(rawValue: adFormat)
|
|
))
|
|
}
|
|
|
|
static func stringParam(_ key: String, in step: WorkflowStep) -> String? {
|
|
guard case let .string(value)? = step.paramValues[key] else { return nil }
|
|
return value
|
|
}
|
|
|
|
/// The backend serializes mediators in lowercase (`admob`) while the SDK's canonical constants are
|
|
/// mixed case (`AdMob`), and `MediatorName` equality is case-sensitive. Map mediators checkpoints
|
|
/// support onto their canonical instance so presenters can match on them; anything else (including
|
|
/// unsupported mediators like AppLovin) passes through untouched.
|
|
private static let knownMediators: [MediatorName] = [.adMob]
|
|
|
|
private static func normalizedMediator(_ rawValue: String) -> MediatorName {
|
|
return Self.knownMediators.first { known in
|
|
known.rawValue.caseInsensitiveCompare(rawValue) == .orderedSame
|
|
} ?? MediatorName(rawValue: rawValue)
|
|
}
|
|
|
|
}
|