Implementing Subscription Management Screen in Mobile Applications
The subscription management screen is not just a page with a "Cancel" button. It's the point where users decide to stay or leave. An incorrectly implemented screen accelerates cancellations: users don't understand what they'll lose, when access expires, or if they can get a discount.
Data to Display
Minimum set:
- Current tier (name, price, period)
- Next billing date or expiration date if cancelled
- Status: active / cancelled (but access until) / grace period / billing retry
- Button to switch to another tier (upgrade/downgrade)
- Link to Apple/Google system screen for cancellation
Reading Current State (StoreKit 2)
import StoreKit
struct SubscriptionInfo {
let productID: String
let displayName: String
let price: String
let renewalDate: Date?
let expirationDate: Date?
let willAutoRenew: Bool
let state: Product.SubscriptionInfo.RenewalState
let isInGracePeriod: Bool
}
func loadSubscriptionInfo(productIds: [String]) async -> SubscriptionInfo? {
guard let product = try? await Product.products(for: productIds).first,
let status = try? await product.subscription?.status.first else {
return nil
}
let renewalInfo = try? status.renewalInfo.payloadValue
let transaction = try? status.transaction.payloadValue
return SubscriptionInfo(
productID: product.id,
displayName: product.displayName,
price: product.displayPrice,
renewalDate: renewalInfo?.renewalDate,
expirationDate: transaction?.expirationDate,
willAutoRenew: renewalInfo?.willAutoRenew ?? false,
state: status.state,
isInGracePeriod: status.state == .inGracePeriod
)
}
renewalInfo.willAutoRenew shows if auto-renewal is enabled. If user cancelled subscription, this field will be false, but access is active until expirationDate.
SwiftUI Screen Component
struct SubscriptionManagementView: View {
@StateObject private var viewModel = SubscriptionManagementViewModel()
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 24) {
if let info = viewModel.subscriptionInfo {
CurrentPlanCard(info: info)
if info.state == .inGracePeriod {
GracePeriodWarning()
}
if info.willAutoRenew, let date = info.renewalDate {
Text("Next billing: \(date.formatted(.dateTime.day().month().year()))")
.foregroundStyle(.secondary)
} else if let date = info.expirationDate {
Text("Access active until: \(date.formatted(.dateTime.day().month().year()))")
.foregroundStyle(.secondary)
}
PlanPickerSection(currentPlan: info.productID)
}
ManageButton()
}
.padding()
}
.task { await viewModel.load() }
}
}
System Subscription Management Button (Mandatory)
Apple requires that in the app there be access to the system subscription management screen. Without this — rejection per guideline 3.1.2:
Button("Manage Subscription in App Store") {
Task {
try? await AppStore.showManageSubscriptions(in: windowScene)
}
}
On Android — deeplink to Google Play:
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("https://play.google.com/store/account/subscriptions?sku=premium_monthly&package=${packageName}")
}
startActivity(intent)
Cancellation Flow — Not Just a Button
Retention best practices when user attempts to cancel:
- Before going to system screen — show what user loses (feature list)
- If subscription is long-standing — offer pause instead of cancellation (Google Play supports natively)
- For long-time users — offer Promotional Offer with discount
This is not a dark pattern — it's honest reminder of value. Important not to overuse: one confirmation screen maximum.
What's Included in the Work
- Reading status via StoreKit 2 / Google Play Billing Library
- Display component: tier, date, status, grace period
- Plan switcher (upgrade/downgrade) with terms description
- Integration of
AppStore.showManageSubscriptions/ Play Store deeplink - Optional cancellation flow with retention offer
Timeline
2–3 days for basic screen with current status. With complex cancellation flow and retention offers — up to 5 days. Pricing is calculated individually.







