Antigravity × App Clip, WidgetKit & Live Activities: Designing an iOS Monetization Funnel
Learn how to design an iOS monetization funnel using App Clip, WidgetKit, and Live Activities with Antigravity. From cold user acquisition to paid conversion, covering StoreKit 2 and RevenueCat integration.
Setup and context: Three Touchpoints, One Revenue Funnel
Most indie iOS developers think about monetization as a straight line: App Store listing → download → in-app purchase. But real user behavior is far messier than that. Users discover apps through QR codes, browse the home screen, and respond to timely notifications — all at different stages of their intent to buy.
App Clip, WidgetKit, and Live Activities each intercept a user at a different moment. When you design them as stages in a unified monetization funnel rather than isolated features, you create a compounding system that dramatically improves your free-to-paid conversion rate.
This guide walks through how to build that system with Antigravity. We'll cover architecture design, concrete SwiftUI code, StoreKit 2 / RevenueCat integration, and an analytics loop for continuous improvement.
Who This Guide Is For
Mid-to-senior iOS developers comfortable with SwiftUI and StoreKit 2
Indie developers looking to improve revenue from existing apps
Developers who want to use Antigravity to ship production-quality code faster
Real Funnel Numbers — Lessons from a 50M-Download Solo App Business
Before diving into the design, let me share the numbers that actually shifted after I started running this funnel in production. Without this grounding, the article would amount to a slight reshuffling of Apple's sample code, so I want to put the numbers first.
I have been shipping iOS and Android apps as a solo developer since 2014, and cumulative downloads recently passed 50 million across the lineup. I currently run six apps — wallpaper, calming, and intention-themed — monetized primarily through AdMob with a subscription layer. The shift after introducing Antigravity, and especially after restructuring the three touchpoints into a real funnel, was largest in "the distance a cold user travels before they convert."
Day 1 conversion by touchpoint (wallpaper app, monthly average on iOS 26):
Funnel stage
Surface
Day 1 conversion
Sample size
TOFU (pre-install)
App Clip session
1.4 %
≈ 24,000 launches
MOFU (home-screen resident)
Widget tap → app open
2.1 %
≈ 8,300 taps
BOFU (active session)
Live Activity → subscription
4.7 %
≈ 1,800 sessions
Control (baseline)
App Store → first open
0.7 %
≈ 41,000 launches
Across the staged funnel, cold-user Day 1 conversion moved from 0.7 % to a weighted 2.3 % — about 3.3× the baseline.
The same restructure took total quarterly revenue up about 1.7×. AdMob eCPM moved only marginally (¥320 → ¥360), so the lift was almost entirely subscription conversion and retention. On the RevenueCat dashboard, Month 1 retention rose from 38 % to 51 %, and Month 3 retention rose from 21 % to 34 %.
What Antigravity actually shortened, in wall-clock terms:
Building App Clip + Widget + Live Activities simultaneously across three targets: ≈ 2 weeks → 4 days of solo time
The wider the band Antigravity can absorb, the more I spend my hours on UX decisions — where the three stages divide, how upgrade prompts are worded, how long the free trial lasts. For an indie developer, that shift is the biggest behavioral change AI agents have brought into my workflow.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Real funnel numbers from a 50M-DL solo app business: lifting Day 1 conversion from 0.7% to 2.3% with App Clip → Widget → Live Activities staging
✦RevenueCat + StoreKit 2 integration patterns that flipped revenue mix from 78%/22% (ads/IAP) to 41%/59%, plus the analytics wiring to keep it that way
✦Six operational pitfalls Apple does not document: App Group write contention, pushToken race, staleDate freezes, sandbox transaction order, RevenueCat hot-path stalls, ad-id mismatch
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
App Clip (TOFU — Top of Funnel): First contact with non-installed users. Delivered via QR codes, NFC, or Safari Smart App Banners, it must deliver value within 10 seconds and drive the user toward the full app.
Widget (MOFU — Middle of Funnel): Ongoing engagement with installed users. Visible on the home screen or lock screen, it keeps your app top-of-mind and warms users toward premium features.
Live Activity (BOFU — Bottom of Funnel): A timely push for high-intent users. Paired with Dynamic Island, it creates an urgency moment that closes the conversion window.
The power of this system comes from how the three stages feed each other — and how Antigravity lets you build all of them without burning out.
Step 1: Configuring Antigravity for the Funnel
Define Funnel Requirements in AGENTS.md
Before writing any code, define your architecture constraints in AGENTS.md at the project root. This file is read by Antigravity's agents on every task, keeping context consistent across sessions.
# Project: MonetizationFunnel## Architecture OverviewThree-stage iOS monetization funnel:1. App Clip → cold user acquisition2. Widget → engagement & upsell signals3. Live Activity → time-sensitive conversion## Key Constraints- App Clip bundle size: < 15MB (hard limit)- Widget timeline: refresh every 15-60 min- Live Activity: use ActivityKit + Push to Start## Monetization Stack- StoreKit 2 for IAP- RevenueCat for subscription management- Firebase Analytics for funnel tracking## Code Style- SwiftUI only (no UIKit)- Swift Concurrency (async/await)- MVVM + Repository pattern
Prompting Antigravity Effectively
With AGENTS.md in place, Antigravity picks up the full context when you ask for implementation help:
Implement the monetization funnel for an iOS app with App Clip,
Widget, and Live Activities.
Requirements:
- Target: iOS 17+
- Payments: RevenueCat + StoreKit 2
- Analytics: Firebase Analytics
- Architecture: see AGENTS.md
Start with the Xcode target configuration and
SharedKit framework design.
Antigravity will analyze the existing project and deliver a design that fits your constraints — rather than generating generic boilerplate.
Step 2: Shared Framework Design
All four targets — the main app, App Clip, WidgetExtension, and LiveActivityExtension — need to share data and business logic. Build a SharedKit framework to avoid duplication.
The widget and the main app (and the App Clip) communicate through a shared App Group container:
// SharedKit/Extensions/AppGroup.swiftimport Foundationpublic enum AppGroup { public static let identifier = "group.com.example.myapp" public static var defaults: UserDefaults { UserDefaults(suiteName: identifier) ?? .standard } public enum Keys { public static let userPremiumStatus = "userPremiumStatus" public static let lastActiveContent = "lastActiveContent" public static let conversionStage = "conversionStage" public static let widgetRefreshDate = "widgetRefreshDate" }}public enum PurchaseState: String, Codable { case none case trial case active case expired}
When you ask Antigravity to "implement the App Group data sharing layer," it uses this structure as context and generates a complete, production-ready implementation — saving you roughly 60–70% of the implementation time.
Step 3: App Clip — TOFU Optimization
The cardinal rule of App Clip design: deliver clear value within 10 seconds. This requires simultaneous attention to the 15 MB bundle size limit and the cold-start performance.
App Clip Entry Point
// AppClip/AppClipApp.swiftimport SwiftUIimport SharedKit@mainstruct AppClipApp: App { @StateObject private var store = AppClipStore() var body: some Scene { WindowGroup { AppClipRootView() .environmentObject(store) .onContinueUserActivity(NSUserActivityTypes.appClipActivity) { activity in store.handleActivity(activity) } } }}@MainActorclass AppClipStore: ObservableObject { @Published var contentID: String? @Published var showUpgradePrompt = false func handleActivity(_ activity: NSUserActivity) { guard let url = activity.webpageURL else { return } parseInvocationURL(url) trackFunnelEvent(.appClipOpened) } private func parseInvocationURL(_ url: URL) { let components = URLComponents(url: url, resolvingAgainstBaseURL: true) contentID = components?.queryItems?.first(where: { $0.name == "content" })?.value } func trackFunnelEvent(_ stage: FunnelStage) { AnalyticsService.shared.logEvent( "funnel_stage", parameters: ["stage": stage.rawValue, "source": "app_clip"] ) }}
Conversion UI: Experience → Interest → Upgrade
// AppClip/Views/AppClipRootView.swiftimport SwiftUIimport SharedKitstruct AppClipRootView: View { @EnvironmentObject var store: AppClipStore var body: some View { NavigationStack { ZStack { if let contentID = store.contentID { ContentPreviewView(contentID: contentID) .transition(.opacity) } else { DefaultClipLandingView() } VStack { Spacer() AppClipUpgradeBanner( onGetFullApp: { store.trackFunnelEvent(.upgradePromptTapped) openFullApp() } ) .padding(.bottom, 20) } } } .onAppear { store.trackFunnelEvent(.appClipView) } } private func openFullApp() { guard let url = URL(string: "https://apps.apple.com/app/id\(AppConstants.appStoreID)") else { return } UIApplication.shared.open(url) }}struct AppClipUpgradeBanner: View { let onGetFullApp: () -> Void var body: some View { VStack(spacing: 12) { Text("Try the full experience — free") .font(.headline) Text("App Clip shows a preview. The full app unlocks everything.") .font(.caption) .foregroundStyle(.secondary) Button("Download the Full App", action: onGetFullApp) .buttonStyle(.borderedProminent) .controlSize(.large) } .padding(20) .background(.regularMaterial) .clipShape(RoundedRectangle(cornerRadius: 16)) .shadow(radius: 8) }}
Bundle Size Optimization with Antigravity
When your App Clip approaches the 15 MB limit, prompt Antigravity directly:
Analyze the App Clip target and reduce its bundle size below 15MB.
Current breakdown:
- SwiftUI views: ~4MB
- Image assets: ~8MB
- SharedKit: ~2MB
Apply these strategies:
1. Lazy-load images via URLCache
2. Slim SharedKit for App Clip (exclude unused modules)
3. Remove unnecessary SDKs (e.g., Firebase Crashlytics)
Antigravity will inspect the project structure and propose specific file-level changes — not just generic advice.
Step 4: WidgetKit — MOFU Engagement
Widgets are your always-on touchpoint with installed users. Thoughtfully designed widgets keep your app visible on the home screen and warm users toward premium features over time.
.onOpenURL { url in guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } switch components.host { case "upgrade": let source = components.queryItems?.first(where: { $0.name == "source" })?.value ?? "unknown" AnalyticsService.shared.logEvent("upgrade_intent", parameters: [ "source": source, "funnel_stage": "widget_cta" ]) showPaywall(source: source) case "content": let contentID = components.path.dropFirst() navigateToContent(String(contentID)) default: break }}
Step 5: Live Activities — BOFU Conversion
Live Activities display real-time information on the Dynamic Island and lock screen. For monetization, they shine during time-limited sales or events — creating urgency at exactly the right moment.
ActivityKit Data Model
// SharedKit/Models/LiveActivityState.swiftimport ActivityKitimport SwiftUIpublic struct SaleActivityAttributes: ActivityAttributes { public struct ContentState: Codable, Hashable { var remainingSeconds: Int var currentPrice: String var discountRate: Int var isPurchased: Bool } public var saleName: String public var originalPrice: String public var deepLinkURL: String}public class LiveActivityService: ObservableObject { private var currentActivity: Activity<SaleActivityAttributes>? public func startSaleActivity( saleName: String, durationSeconds: Int, discountRate: Int, originalPrice: String, salePrice: String ) async { guard ActivityAuthorizationInfo().areActivitiesEnabled else { return } let attributes = SaleActivityAttributes( saleName: saleName, originalPrice: originalPrice, deepLinkURL: "myapp://upgrade?source=live_activity&sale=\(saleName)" ) let initialState = SaleActivityAttributes.ContentState( remainingSeconds: durationSeconds, currentPrice: salePrice, discountRate: discountRate, isPurchased: false ) do { let activity = try Activity.request( attributes: attributes, content: .init( state: initialState, staleDate: Date().addingTimeInterval(Double(durationSeconds)) ) ) currentActivity = activity startCountdown(durationSeconds: durationSeconds) } catch { print("Live Activity failed to start: \(error)") } } private func startCountdown(durationSeconds: Int) { Task { for remaining in stride(from: durationSeconds, through: 0, by: -1) { guard let activity = currentActivity else { break } let updatedState = SaleActivityAttributes.ContentState( remainingSeconds: remaining, currentPrice: activity.content.state.currentPrice, discountRate: activity.content.state.discountRate, isPurchased: false ) await activity.update(ActivityContent(state: updatedState, staleDate: nil)) if remaining > 0 { try? await Task.sleep(nanoseconds: 1_000_000_000) } } await endActivity(purchased: false) } } public func endActivity(purchased: Bool) async { guard let activity = currentActivity else { return } let finalState = SaleActivityAttributes.ContentState( remainingSeconds: 0, currentPrice: activity.content.state.currentPrice, discountRate: activity.content.state.discountRate, isPurchased: purchased ) await activity.end( ActivityContent(state: finalState, staleDate: nil), dismissalPolicy: .after(.now.addingTimeInterval(10)) ) currentActivity = nil }}
Dynamic Island and Lock Screen UI
// LiveActivityExtension/LiveActivityViews.swiftimport SwiftUIimport ActivityKitimport WidgetKitstruct SaleActivityWidget: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: SaleActivityAttributes.self) { context in LockScreenSaleView(context: context) } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { SaleTimerView(seconds: context.state.remainingSeconds) } DynamicIslandExpandedRegion(.trailing) { DiscountBadge(rate: context.state.discountRate) } DynamicIslandExpandedRegion(.bottom) { Link(destination: URL(string: context.attributes.deepLinkURL)!) { Label("Buy Now — \(context.state.currentPrice)", systemImage: "cart.fill") .font(.callout.bold()) .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, 8) .background(.blue.gradient) .clipShape(RoundedRectangle(cornerRadius: 12)) } } } compactLeading: { Image(systemName: "timer") .foregroundStyle(.orange) } compactTrailing: { Text(formatTime(context.state.remainingSeconds)) .font(.caption.monospacedDigit()) .foregroundStyle(.orange) } minimal: { Image(systemName: "percent") .foregroundStyle(.orange) } } } private func formatTime(_ seconds: Int) -> String { let m = seconds / 60 let s = seconds % 60 return String(format: "%02d:%02d", m, s) }}struct SaleTimerView: View { let seconds: Int var body: some View { HStack(spacing: 2) { Image(systemName: "clock") .font(.caption2) Text(formattedTime) .font(.caption.monospacedDigit()) } .foregroundStyle(.red) } var formattedTime: String { let h = seconds / 3600 let m = (seconds % 3600) / 60 let s = seconds % 60 return h > 0 ? String(format: "%dh %02dm", h, m) : String(format: "%02d:%02d", m, s) }}
Step 6: RevenueCat + StoreKit 2 Integration
All three funnel stages eventually route the user to a paywall. RevenueCat provides a unified layer for subscription management, purchase restoration, and entitlement checking across iOS versions.
RevenueCat Setup
// MainApp/AppSetup.swiftimport RevenueCatimport FirebaseCorefunc setupApp() { FirebaseApp.configure() Purchases.configure(withAPIKey: "YOUR_REVENUECAT_API_KEY") Purchases.logLevel = .warn if let userID = AuthService.shared.currentUserID { Purchases.shared.logIn(userID) { customerInfo, _, error in if let error { print("RevenueCat login error: \(error)"); return } updatePremiumStatus(from: customerInfo) } }}func updatePremiumStatus(from customerInfo: CustomerInfo?) { guard let info = customerInfo else { return } let state: PurchaseState if info.entitlements["premium"]?.isActive == true { state = .active } else if info.entitlements["trial"]?.isActive == true { state = .trial } else { state = .none } AppGroup.defaults.set(state.rawValue, forKey: AppGroup.Keys.userPremiumStatus) WidgetCenter.shared.reloadAllTimelines() // Refresh widget immediately}
The funnel is only as good as your ability to measure and iterate on it. Here's how to instrument it properly from day one.
Firebase Analytics Funnel Events
// SharedKit/Services/AnalyticsService.swiftpublic class AnalyticsService { public static let shared = AnalyticsService() public enum FunnelEvent: String { // TOFU case appClipOpened = "app_clip_opened" case appClipContentViewed = "app_clip_content_viewed" case appClipUpgradeTapped = "app_clip_upgrade_tapped" // MOFU case widgetTapped = "widget_tapped" case widgetUpgradeCTATapped = "widget_upgrade_cta_tapped" // BOFU case liveActivityOpened = "live_activity_opened" case liveActivityCTATapped = "live_activity_cta_tapped" // Conversion case paywallViewed = "paywall_viewed" case purchaseStarted = "purchase_started" case purchaseCompleted = "purchase_completed" case purchaseFailed = "purchase_failed" } public func logFunnelEvent( _ event: FunnelEvent, source: String, additionalParams: [String: Any] = [:] ) { var params: [String: Any] = [ "source": source, "timestamp": Date().timeIntervalSince1970 ] params.merge(additionalParams) { _, new in new } Analytics.logEvent(event.rawValue, parameters: params) }}
A/B Testing Widget Copy with Antigravity
Once your analytics are flowing, let Antigravity help you run controlled experiments:
Implement an A/B test for the Widget upgrade CTA copy.
Variants:
- A: "Try Premium"
- B: "7 Days Free"
- C: "Upgrade Now"
Requirements:
- Use Firebase Remote Config to manage variants
- Deterministic assignment by user ID (33% each)
- Track click-through rate in Firebase Analytics
- Apply on each Widget timeline refresh
Antigravity will generate a complete Remote Config integration that you can deploy without touching the App Store submission process.
Six Pitfalls Apple's Documentation Skips Over
Here are six failure modes that show up only after a year of running this in production. They are not in the WWDC sessions or the official samples. Hold these as a human-side checklist before you let Antigravity scaffold the code — it sharpens the reviews enormously.
1. App Group UserDefaults writes are not atomic
Apple's pattern of sharing UserDefaults(suiteName:) between an App Clip and the main app assumes a single writer at a time. When both processes write simultaneously the behavior is effectively undefined. In one of my wallpaper apps, when a Live Activity update() raced with an App Clip set(), the value silently dropped about 0.3 % of the time.
The fix is to keep writes one-directional. The App Clip writes; the main app reads only. If you do need to update from the main app, route it through CloudKit or App Intents on a separate key.
// ❌ Two-way writes will racesharedDefaults.set(["sessionId": id], forKey: "active_session") // App ClipsharedDefaults.set(["lastSeen": Date()], forKey: "active_session") // Main app// ✅ App Clip writes; main app is read-only on that keysharedDefaults.set(payload, forKey: "clip_handoff_v2")// Main app reads only:let payload = sharedDefaults.dictionary(forKey: "clip_handoff_v2")// To persist new runtime state, use a separate key:sharedDefaults.set(updated, forKey: "main_runtime_state_v2")
2. pushTokenUpdates is an AsyncSequence that can miss the first emission
Subscribing to Activity.pushTokenUpdates is the standard pattern for sending the APNs token to your server. If you start that Task immediately after request(), the very first token update can be missed — especially under iPad debug builds.
The reliable pattern is to upsert the initial token from activity.pushToken and also subscribe to the stream. The server upserts on (activity_id) so either path lands the same record.
let activity = try Activity<DeliveryAttributes>.request( attributes: .init(orderId: orderId), content: .init(state: state, staleDate: .now.addingTimeInterval(60 * 60)), pushType: .token)// Upsert the initial token immediatelyif let token = activity.pushToken { await api.upsertPushToken(activity.id, token: token, source: "initial")}// Subscribe to subsequent updatesTask { for await tokenData in activity.pushTokenUpdates { await api.upsertPushToken(activity.id, token: tokenData, source: "stream") }}
3. Leaving staleDate unset turns the lock screen into a frozen card
If staleDate is nil, a delayed or failed APNs push leaves the lock-screen activity displaying stale information forever. Users decide the app is broken and uninstall in a single tap. I learned this not from Crashlytics but from an App Store review that read "the widget freezes on the lock screen."
Set staleDate to roughly 1.5× the longest update interval a user could reasonably expect — 60 minutes for delivery tracking, 24 hours for a subscription offer banner.
After staleDate iOS dims the card automatically, which gives users a clear visual cue that the data is old.
4. StoreKit 2 transaction order differs between sandbox and production
The order in which Transaction.currentEntitlements emits varies between sandbox (especially via the StoreKit testing harness) and production. Sandbox tends to surface only active purchases; production may once replay revoked transactions too. Forget that, and your UI marks an expired subscription as active.
Always compare expirationDate to .now and treat revocationDate as a hard exclude. When Antigravity scaffolds StoreKit 2 code, this is the check it most often skips, so it goes on my review checklist.
for await result in Transaction.currentEntitlements { guard case .verified(let tx) = result else { continue } if let exp = tx.expirationDate, exp < .now { continue } if tx.revocationDate != nil { continue } await entitlements.upsert(productId: tx.productID)}
5. getCustomerInfo() on the hot path locks up the UI
RevenueCat caches network responses internally, but calling getCustomerInfo() four or five times during the App Clip cold-start sequence can serialize on the SDK's internal lock and block the main thread. In my measurements that delays Cold-start First Action by about 1.2 seconds.
Fetch it once during startup, cache it on a @MainActor singleton, and call Purchases.configure()before the first piece of UI is presented.
6. Mismatched user IDs between App Clip and the main app destroy the funnel report
This was the most damaging mistake I made early on. ASIdentifierManager.advertisingIdentifier is not guaranteed to match between an App Clip and its parent app, so Firebase and Mixpanel record the same human as two different users. The "App Clip → main app launch" conversion in my funnel report read essentially 0 % and made every experiment unmeasurable.
Generate a session UUID inside the App Clip, write it to App Group, and have the main app adopt it on first launch.
// Inside the App Clip view (onAppear)let sessionId = UUID().uuidStringsharedDefaults.set(sessionId, forKey: "clip_handoff_session_id")Analytics.setUserID(sessionId)// In the main app's first-launch sequenceif let inherited = sharedDefaults.string(forKey: "clip_handoff_session_id") { Analytics.setUserID(inherited) sharedDefaults.removeObject(forKey: "clip_handoff_session_id")}
This single piece of plumbing lets you measure "App Clip-acquired user Day 7 conversion" reliably for the first time.
My Recommended Rollout — Sequencing Without Breaking Yourself
From 50 million downloads of operational experience, my recommendation is not to add all three features at once. Even with Antigravity, simultaneous work across three extension targets escalates the human review load fast.
Week 1: stabilize StoreKit 2 + RevenueCat in the main app. Let Antigravity scaffold the Transaction.updates subscription and the single getCustomerInfo() entry point, then review carefully.
Week 2: add one Widget. Start with a Home Screen Medium widget rather than a Lock Screen widget — the cognitive load is lower and the data-flow surface is smaller.
Week 3: add the App Clip. Before any QR / NFC distribution, ship it through TestFlight and verify the ?session= parameter actually closes the loop in your Mixpanel dashboard. Do not skip this verification.
Week 4: add Live Activities. Now you are bringing APNs into the picture; make sure your server-side push is idempotent. Start with a conservative staleDate and lengthen it as you learn the real cadence.
At the end of each week, look at the equivalent of an activity.cycle span (I instrument this with OpenTelemetry) and feed average duration and failure rate into Honeycomb / Grafana — that becomes the input for the next week's decisions.
What I let Antigravity write vs. what I write myself:
Let it write: SwiftUI / StoreKit 2 / Widget / Live Activity boilerplate, APNs payload assembly, RevenueCat hook callbacks.
Write myself: the funnel-stage boundaries, conversion copy, thresholds for staleDate and relevanceScore, A/B test hypotheses.
Three months in, my time split between "writing code" and "thinking about design and UX" flipped from roughly 7:3 to 3:7. That single shift is, more than any feature, why funnel conversion is up 3×.
Next Step — One Thing to Build This Week
Thank you for staying with me to the end. To close out, here is the single most valuable next step.
Start by letting Antigravity scaffold only the StoreKit 2 / RevenueCat reconciliation in your main app and finish it this week: subscribe to Transaction.updates, judge entitlements through a single chokepoint, and confirm that your server-side entitlements upsert produces identical results in sandbox and production. If that foundation wobbles, every funnel measurement you take on top of App Clip and Live Activities will wobble too.
I hope this is useful for anyone else building iOS monetization solo.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.