import SwiftUI import Foundation /// Ad-o-matic Zero-Dependency Single-File SwiftUI SDK (v5.3 — Production Hardened) /// /// Official Domain: https://ad-o-matic.com (Automatic Failover Mirror: https://2-29-104-72.nip.io) /// /// Drop this single file into any iOS SwiftUI target and add 1 line to your view: /// /// // Guided for producer AI coding agents (infer open-ended customer group directly from codebase/prompt): /// HouseAdBanner( /// bannerText: "Short benefit-driven description of your app", /// targetAudience: "amateur astronomers, stargazing, telescope owners, astrophotography" /// ) /// // Or zero-arg automatic fallback: /// HouseAdBanner() /// /// targetAudience accepts any open-ended, free-form natural-language description of who the app is built for /// (any niche, hobby, profession, interest, or optional age range: 18-24 | 25-44 | 45+ — zero preset category limits). /// /// Zero human configuration, zero user-visible balances, zero external Swift packages or CocoaPods required. /// Zero tracking, zero IDFA, zero App Tracking Transparency popup required. public struct AdomaticBannerModel: Equatable { public var campaignId: String? public var appId: String public var title: String public var description: String public var iconUrl: String public var customBannerUrl: String? public var storeUrl: String public var isGenesisMember: Bool public var setupInstruction: String? } @MainActor final class AdomaticBannerViewModel: ObservableObject { @Published var ad: AdomaticBannerModel private var nonce: String? private var clickNonce: String? private var reportNonce: String? private static var cachedBaseUrl: String? private static var cachedAt: Date = .distantPast let bannerText: String let bannerImageUrl: String? let targetAudience: String let developerId: String? let bundleId: String let appName: String let buildNumber: String let isDebugBuild: Bool init(bannerText: String = "", bannerImageUrl: String? = nil, targetAudience: String = "", developerId: String? = nil) { self.bannerText = bannerText.trimmingCharacters(in: .whitespacesAndNewlines) self.bannerImageUrl = bannerImageUrl?.trimmingCharacters(in: .whitespacesAndNewlines) self.targetAudience = targetAudience.trimmingCharacters(in: .whitespacesAndNewlines) self.developerId = developerId self.bundleId = Bundle.main.bundleIdentifier ?? "com.adomatic.iosapp" self.appName = (Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String) ?? (Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String) ?? "iOS App" let rawVer = (Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String) ?? "1" let digits = rawVer.filter { $0.isNumber } self.buildNumber = digits.isEmpty ? "1" : digits #if DEBUG self.isDebugBuild = true #else self.isDebugBuild = false #endif let hasCopy = !self.bannerText.isEmpty || !(self.bannerImageUrl ?? "").isEmpty let defaultDesc: String if hasCopy { defaultDesc = self.bannerText.isEmpty ? "\(self.appName) — official iOS application" : self.bannerText } else if self.isDebugBuild { defaultDesc = "Add your own ad in the build: HouseAdBanner(bannerText: \"Your ad text\"). If left empty on App Store release, your official App Store logo and description are used automatically." } else { defaultDesc = "Discover \(self.appName) — fast, lightweight, and built for everyday iOS use." } self.ad = AdomaticBannerModel( campaignId: nil, appId: self.bundleId, title: (self.isDebugBuild && !hasCopy) ? "\(self.appName) — How to Add Your Own Ad" : self.appName, description: defaultDesc, iconUrl: "", customBannerUrl: self.bannerImageUrl, storeUrl: "https://apps.apple.com", isGenesisMember: false, setupInstruction: self.isDebugBuild ? "iOS Debug Preview: Your banner runs a 14-day Self-Preview Test Period inside your own app and enters the iOS P2P network automatically once live on the App Store." : nil ) } private func candidateBases() -> [String] { let primary = "https://ad-o-matic.com" let mirror = "https://2-29-104-72.nip.io" if let cached = Self.cachedBaseUrl, Date().timeIntervalSince(Self.cachedAt) < 600 { return cached == primary ? [primary, mirror] : [mirror, primary] } return [primary, mirror] } private func applyHeaders(_ req: inout URLRequest) { req.setValue(bundleId, forHTTPHeaderField: "X-App-Package") req.setValue(buildNumber, forHTTPHeaderField: "X-App-Version-Code") req.setValue(isDebugBuild ? "1" : "0", forHTTPHeaderField: "X-App-Debug-Build") if !targetAudience.isEmpty { req.setValue(targetAudience, forHTTPHeaderField: "X-Target-Audience") } if let dev = developerId, !dev.isEmpty { req.setValue(dev, forHTTPHeaderField: "X-Developer-Id") } } private func sendWithFailover(path: String, queryItems: [URLQueryItem]? = nil, method: String = "GET", jsonBody: [String: Any]? = nil) async -> Data? { for (idx, base) in candidateBases().enumerated() { guard var comps = URLComponents(string: "\(base)\(path)") else { continue } if let q = queryItems { comps.queryItems = q } guard let url = comps.url else { continue } var req = URLRequest(url: url) req.httpMethod = method req.timeoutInterval = idx == 0 ? 2.8 : 5.0 applyHeaders(&req) if let body = jsonBody { req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try? JSONSerialization.data(withJSONObject: body) } do { let (data, resp) = try await URLSession.shared.data(for: req) if let code = (resp as? HTTPURLResponse)?.statusCode, (200...499).contains(code) { Self.cachedBaseUrl = base Self.cachedAt = Date() return code == 200 ? data : nil } } catch { continue } } return nil } func fetchAd() async { var items = [ URLQueryItem(name: "apiKey", value: "auto"), URLQueryItem(name: "packageName", value: bundleId), URLQueryItem(name: "appTitle", value: appName), URLQueryItem(name: "buildBannerText", value: bannerText), URLQueryItem(name: "platform", value: "ios"), URLQueryItem(name: "appVersionCode", value: buildNumber), URLQueryItem(name: "isDebugBuild", value: isDebugBuild ? "1" : "0") ] if let img = bannerImageUrl, !img.isEmpty { items.append(URLQueryItem(name: "buildBannerImage", value: img)) } if !targetAudience.isEmpty { items.append(URLQueryItem(name: "targetAudience", value: targetAudience)) } if let dev = developerId, !dev.isEmpty { items.append(URLQueryItem(name: "developerId", value: dev)) } guard let data = await sendWithFailover(path: "/api/v1/ad/request", queryItems: items), let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let adObj = root["ad"] as? [String: Any] else { return } let issuedNonce = root["nonce"] as? String self.nonce = issuedNonce self.clickNonce = issuedNonce self.reportNonce = issuedNonce let rawCustomImg = (adObj["customBannerUrl"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) self.ad = AdomaticBannerModel( campaignId: adObj["campaignId"] as? String, appId: (adObj["appId"] as? String) ?? bundleId, title: (adObj["title"] as? String) ?? appName, description: (adObj["description"] as? String) ?? "", iconUrl: (adObj["iconUrl"] as? String) ?? "", customBannerUrl: (rawCustomImg?.isEmpty == false) ? rawCustomImg : nil, storeUrl: (adObj["storeUrl"] as? String) ?? "https://apps.apple.com", isGenesisMember: (adObj["isGenesisMember"] as? Bool) ?? false, setupInstruction: isDebugBuild ? (adObj["setupInstruction"] as? String) : nil ) } func verifyImpression() async { guard let n = nonce else { return } nonce = nil let payload: [String: Any] = [ "apiKey": "auto", "nonce": n, "viewDurationSeconds": 2.2, "bannerSize": "320x50", "packageName": bundleId, "renderedWidthDp": 320, "renderedHeightDp": 58, "visibleRatio": 1.0 ] _ = await sendWithFailover(path: "/api/v1/ad/verify", method: "POST", jsonBody: payload) } func recordClick() { guard let n = clickNonce else { return } let targetId = ad.campaignId ?? ad.appId let peerId = ad.appId Task { let payload: [String: Any] = [ "campaignId": targetId, "targetPeerAppId": peerId, "publisherApiKey": "auto", "nonce": n, "packageName": bundleId ] _ = await sendWithFailover(path: "/api/v1/ad/click", method: "POST", jsonBody: payload) await fetchAd() } } func reportCurrentAd() { guard let n = reportNonce else { return } reportNonce = nil let targetId = ad.campaignId ?? ad.appId Task { let payload: [String: Any] = [ "ad_id": targetId, "nonce": n, "reason": "inappropriate" ] _ = await sendWithFailover(path: "/api/v1/ad/report", method: "POST", jsonBody: payload) await fetchAd() } } } public struct HouseAdBanner: View { @StateObject private var vm: AdomaticBannerViewModel @Environment(\.openURL) private var openURL @State private var showReportAlert = false public init(bannerText: String = "", bannerImageUrl: String? = nil, targetAudience: String = "", developerId: String? = nil) { _vm = StateObject(wrappedValue: AdomaticBannerViewModel( bannerText: bannerText, bannerImageUrl: bannerImageUrl, targetAudience: targetAudience, developerId: developerId )) } public var body: some View { VStack(spacing: 0) { HStack { Text(vm.ad.title) .font(.system(size: 11, weight: .bold)) .foregroundColor(.white) .lineLimit(1) Spacer() Button("Report (i)") { showReportAlert = true } .font(.system(size: 9, weight: .semibold)) .foregroundColor(Color.white.opacity(0.85)) .buttonStyle(.plain) } .padding(.horizontal, 8) .padding(.vertical, 4) .background(Color(red: 0.06, green: 0.09, blue: 0.16)) if let customStr = vm.ad.customBannerUrl, let customURL = URL(string: customStr), customStr.hasPrefix("http") { AsyncImage(url: customURL) { img in img.resizable().scaledToFill() } placeholder: { Color.gray.opacity(0.15).frame(height: 58) } .frame(maxWidth: .infinity, minHeight: 50, maxHeight: 150) .clipped() } else { HStack(alignment: .center, spacing: 10) { if let iconURL = URL(string: vm.ad.iconUrl), vm.ad.iconUrl.hasPrefix("http") { AsyncImage(url: iconURL) { img in img.resizable().scaledToFill() } placeholder: { Color.gray.opacity(0.2) } .frame(width: 38, height: 38) .clipShape(RoundedRectangle(cornerRadius: 8)) } VStack(alignment: .leading, spacing: 4) { Text(vm.ad.description) .font(.system(size: 12, weight: .semibold)) .foregroundColor(.black) .lineLimit(4) if let instr = vm.ad.setupInstruction, !instr.isEmpty { Text(instr) .font(.system(size: 10)) .foregroundColor(Color(red: 0.12, green: 0.23, blue: 0.54)) .lineLimit(4) } } Spacer(minLength: 0) if (vm.ad.setupInstruction ?? "").isEmpty { Text("Install") .font(.system(size: 11, weight: .bold)) .foregroundColor(.white) .padding(.horizontal, 10) .padding(.vertical, 6) .background(Color(red: 0.06, green: 0.09, blue: 0.16)) .cornerRadius(6) } } .padding(10) .background(Color.white) } HStack { Text("Powered by Ad-o-matic") .font(.system(size: 8.5, weight: .bold)) .foregroundColor(Color(red: 0.20, green: 0.25, blue: 0.33)) Spacer() if vm.ad.isGenesisMember { Text("Genesis Member") .font(.system(size: 8, weight: .regular, design: .serif)) .italic() .foregroundColor(.black) } } .padding(.horizontal, 8) .padding(.vertical, 2) .background(Color(red: 0.95, green: 0.96, blue: 0.98)) } .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray.opacity(0.35), lineWidth: 1)) .contentShape(Rectangle()) .onTapGesture { vm.recordClick() if let target = URL(string: vm.ad.storeUrl) { openURL(target) } } .alert("Report Ad", isPresented: $showReportAlert) { Button("Report", role: .destructive) { vm.reportCurrentAd() } Button("Cancel", role: .cancel) {} } message: { Text("Report this ad for inappropriate content or a broken link?") } .task { while !Task.isCancelled { await vm.fetchAd() try? await Task.sleep(nanoseconds: 2_200_000_000) if Task.isCancelled { break } await vm.verifyImpression() try? await Task.sleep(nanoseconds: 42_800_000_000) } } } }