Block · Account

New

Plan and Usage SwiftUI Block

The subscription section of Settings: the plan and its renewal, usage meters that warn near a limit, and the way up.

Where a subscriber checks their plan. A summary card names the plan with a tinted status chip, its price and when it renews, ends or leaves its trial; usage meters fill to what has been used and turn to warning and danger near a limit. Upgrade works, confirms and folds away on the top plan, and Manage subscription and Restore purchases use StoreKit by default.

  • SwiftUI
  • iOS 17.0+
  • plan-usage
Get SwiftPieces Pro

$199 once, every piece included. See how installing works · Sign in

01

What's included

SwiftUI source you own, built on Apple frameworks only.

  • The plan with a status chip, price and renewal date
  • Usage meters that warn near a limit and show when allowances reset
  • Upgrade with working, done and failed shown in place
  • Manage subscription and Restore purchases through StoreKit
  • Loading, payment issue and free plan states from one PlanUsage

Requires

In-App Purchase capability

02

Implementation notes

How it behaves, what it handles, and the decisions behind it.

A summary card names the plan, with a tinted status chip and, under a hairline, its price and the date it renews, ends or leaves its trial; a payment issue turns that line to the danger colour. The Usage card holds a meter for each limit: a symbol and label, the figure ("8.2 of 10 GB") and a thin bar that fills when the block appears and moves whenever you pass a new value. A meter that passes its warning share fills first, then turns to the warning colour and says "Almost full"; at the limit it turns to the danger colour and says "Limit reached". Allowances read "Resets Nov 2" under the bar. Upgrade carries the plan's own call to action with what you get under it, shows progress while your closure runs and a checkmark when it returns, then folds away once you pass the top plan, while the summary and meters settle on the new one. A thrown error shows its message under the button. Manage subscription opens the App Store's own sheet, and Restore purchases runs AppStore.sync() with its working, done and failed states. Pass nil while billing loads and the block shows grey shapes in its own layout.

Notes

  • PlanUsageBlock(usage:onUpgrade:onManage:onRestore:) takes one PlanUsage and the upgrade closure. It is a section: no navigation stack, background, padding or scroll view of its own, so place it in your own column: a ScrollView with .padding(.horizontal, 20), or a sheet.
  • PlanUsage holds the plan's name, its Price (.amount(9.99, currencyCode: "USD"), .display(product.displayPrice) or .free), the Period (.week, .month, .year, or nil), a Status, the meters, the Upgrade and an optional subscriptionGroupID. PlanUsage.sample is the Plus plan from the preview.
  • Status sets the chip and the date line: .renews(date) ("Active", "Renews Nov 2"), .trial(ends:) ("Free trial", "Trial ends Nov 2"), .ends(date) ("Cancelled", "Ends Nov 2"), .paymentIssue(until:) ("Payment issue", "Update payment by Nov 2" in the danger colour) and .noRenewal for free plans and one-time purchases, which shows no chip and hides Manage subscription. A free plan with nothing to renew leaves the price line out.
  • A PlanUsage.Meter has an id, label, used, limit, unit ("GB", or empty for counts), a format for both figures, an optional SF Symbol and warnAt (0.85 by default). Give it a resets date and it reads as an allowance: "Resets Nov 2" under the bar, then "Running low" and "All used" instead of "Almost full" and "Limit reached". used may pass the limit. fraction and level (.normal, .nearLimit, .atLimit) are there for your own logic.
  • PlanUsage.Upgrade is the button's title ("Upgrade to Studio") and an optional detail line of what you get. Nil on the top plan.
  • onUpgrade is async throws: present your paywall or call product.purchase() there, then hand the block the new PlanUsage. The button shows a spinner until it returns, then a checkmark for a moment. Throwing shows the error's errorDescription (or "The upgrade didn't go through. Try again.") under the button in the danger colour; throw CancellationError() when the person backs out and the button goes back to rest quietly.
  • Without onManage the block presents manageSubscriptionsSheet, on subscriptionGroupID when you set one. Pass onManage for plans billed outside the App Store; then the row also shows on plans that don't renew.
  • Without onRestore the block calls AppStore.sync(). A cancelled App Store sign-in goes back to rest quietly; any other error shows its description or "Couldn't reach the App Store. Try again." Done shows "Purchases restored" for a few seconds. Your app's transaction listener picks up whatever comes back.
  • Haptics mark success when an upgrade or restore finishes and an error when either fails.
  • VoiceOver reads the summary as one element ("Plus plan, $9.99 per month. Active. Renews Nov 2"), each meter as its label with the figure, the percentage and its line, and announces upgrade and restore results. Text grows with Dynamic Type: rows stack their figures under their labels, the chip moves under the name, and symbols step aside at accessibility sizes so text keeps the width.
  • Reduce Motion places the meters at their values without the fill, and state changes become short fades.
  • PlanUsageBlockExample at the end of PlanUsageBlock.swift is the Account screen in the preview: Plus two weeks before it renews, storage climbing into its warning as a backup finishes, and an upgrade to Studio that settles the plan and meters on the new tier. It reads the theme from the environment; pass wrapsInNavigationStack: false to push it in your own stack.

Theming

  • Reads SPTheme from @Environment(\.spTheme) and requires the design-system item.
  • Cards are color.surface with radius.lg, 16 point padding and SPHairline dividers in separator, inset to the text; groups sit 24 points apart (space.xl).
  • The plan's name is the one display moment: the brand's display face (type.face) at 28 points, type.displayWeight capped at semibold, in textPrimary. When the face isn't bundled it falls back to SF in type.figureDesign, or in type.textDesign when figures are monospaced, so a word is never set in SF Mono. Labels and rows use type.body, figures type.subhead with monospaced digits, secondary lines type.footnote, the Usage title type.headline.
  • Text is textPrimary and textSecondary; symbols are textSecondary, the Manage arrow textTertiary.
  • Bars run in textPrimary on a fill track, turning to warning and danger. The status chip is its status colour (success, info, danger) on a 14% tint of it, or textSecondary on fill when cancelled. Restore's done line is success, failures danger.
  • The upgrade button is the accent with accentOn text in type.headline and radius.md, the only accent on the block.
  • Fills move with motion.hero on appear and motion.smooth on change; states cross-fade with motion.snappy. Every animation becomes a short fade under Reduce Motion.

Requirements

  • In-App Purchase capability, for the manage sheet and AppStore.sync(). Neither is needed when you pass onManage and onRestore.