iOS billing: Monthly/Yearly plans, refund request, analytics, unavailable-plans retry, no web purchase links
Merged
LA
23d ago+413+413+012345678901234567890123456789-57-57-01234567890123456789
Summary
App-side work for selling cmux Go, Pro and Max in the App Store app (com.cmux.app, ASC 6783338052).
- Analytics (
BillingModel,BillingAnalyticsEvent):ios_products_loaded{product_ids(comma-separated),storefront,count,billing_period(the loaded periods, comma-separated)}. Sent once for each distinct outcome per account.ios_products_failed{reason,storefront}.reasonis aBillingFailure.analyticsReason, orno_server_products.ios_purchase_succeeded{product_id,plan_id,billing_period,price,currency,environment(Production/Sandbox/Xcode),delivery(accepted|deferred)}. An approved Ask to Buy purchase also sends it.ios_purchase_startedalso carriesbilling_period.ios_refund_request_opened{product_id} andios_refund_request_result{product_id,result:success|userCancelled|error}. Both names are added toweb/services/analytics/iosEventPolicy.ts, which is the only web/ change.
- Monthly / Yearly:
- Each product carries
billingPeriod, taken from the server field (#20587) or else the product-id suffix. The client sendsx-cmux-billing-periods: monthly,yearly. - The plans screen has a segmented Monthly/Yearly picker. Yearly is the default, as on cmux.com/pricing. The picker shows only when a plan is sold in both periods.
- The screen shows one offer per plan. Go is monthly only. Max is monthly only until Max Yearly is priced.
- Yearly offers also show the per-month equivalent ("$58.33 / month, billed yearly").
- The current App Store product comes from StoreKit current entitlements, so Pro Monthly and Pro Yearly are told apart and a user can switch between them.
- The renewal disclosure names monthly or yearly renewal in 9 localizations.
ios/Config/cmux-dev.storekitgains Pro Yearly ($699.99, P1Y, same group level as Pro Monthly).
- Each product carries
- Request a Refund: shown when the plan is billed through the App Store. It opens StoreKit's
.refundRequestSheetfor the latest transaction of the active subscription. "Manage Subscription" uses StoreKit's.manageSubscriptionsSheet. - Bug: if the account was eligible but no products loaded, the spinner never stopped. Now the screen shows "Plans aren't available in the app right now." with Try Again, and the spinner returns while the retry runs.
- Copy (3.1.1 / 3.1.3(b)): the message for web subscribers now reads "You subscribe on the web."
- No external purchase link: without a billing model, the New Machine sheet no longer opens
cmux.com/pricing. - Purchases still carry
appAccountTokenand post the JWS to the server beforefinish().
Verification
cmux-ci run ... run-swift-testing-suites.sh --arg Packages/iOS/CmuxMobileBillingpasses at each head (latest cfee6eb2). The existing exact-name assertions are updated. No new unit tests.- Fleet
cmux-ci build ios(device archive and simulator build) passes at ba640f25, 8352e856 and 40efd056. The build for cfee6eb2 is in the comments. - A fleet simulator run (verify-remote) of the plans screen with no products loading shows the new unavailable state with Try Again.