Repository navigation
Releases: no-problem-dev/swift-subscription
Releases · no-problem-dev/swift-subscription
Release list
3.2.0
Added
SubscriptionConfiguration(allowsTestStoreInReleaseBuilds:). The RevenueCat SDK crashes on
purpose when a Test Store key runs in a build compiled withoutDEBUG(TestFlight included).
Turning this on passes RevenueCat'sforceAllowTestStoreInReleaseBuilds, for a separate
development app that is distributed through TestFlight and can never reach the App Store.
Defaults tofalse, so existing apps behave as before.
3.1.1
Fixed
clearUser()on an identity that is already anonymous (sign-out after asyncUser(userId:)
that failed) used to throwuserSyncFailed, because RevenueCat refuseslogOut()for an
anonymous user, and the entitlement held in memory and in theEntitlementCachewas left
behind. It now skipslogOut()and forgets the entitlement.
3.1.0
Added
- Custom variables for paywalls built in the RevenueCat dashboard.
PaywallVariable
(.string,.number,.bool) lives inSubscriptionUI, so an app can build the values
without linking RevenueCatUI.PaywallContainer(mode:variables:onEntitled:…)hands them to the
RevenueCat paywall, and.paywallVariables(_:)does the same from any ancestor of a
RevenueCatPaywallView. The app's own paywall does not read them. - Placements.
PaywallMode.revenueCat(placement:)and.automatic(placement:)look up the
offering the dashboard serves at a placement (Offerings.currentOffering(forPlacement:)), and
PaywallMode.placementIdentifiernames it. A placement the dashboard does not know falls back
to the current offering; a placement set to serve nothing ends at the app's own paywall. - What was bought. The
onEntitledofPaywallContainer(mode:variables:…)receives the
SubscriptionStatusthe purchase or restore left behind;activePackageIdis the App Store
product identifier. The existing initialiser and its argument-lessonEntitledare unchanged. SubscriptionPackage.productId, the App Store product identifier, and an initialiser that
takes it. The existing initialiser sets it toid.
Fixed
- A package with a custom identifier in the dashboard (
pro.yearlyrather than$rc_annual)
has the package type.custom, and itsdurationused to be.unknownand its
pricePerMonthnil. Both are now read from the product's subscription period: one year or
twelve months is.annual, one month is.monthly.
Changed
- The minimum RevenueCat SDK is 5.90.0 (was 5.19.0): dashboard paywalls with display rules and
offer_price_with_zeroneed it. An app pinned below 5.90.0 stops resolving until it raises
its own requirement. PaywallModeholds a placement. Two modes that differ only in placement are no longer equal,
soPaywallContainerdecides again when the placement changes.
Compatibility
- Source compatible with 3.0.0: no public symbol was removed or changed, and every addition is a
new overload or a new property. Code that constructsSubscriptionPackagewith the existing
initialiser keeps compiling and getsproductId == id. - Behaviour change to check: an app that skipped
.unknownpackages, and whose dashboard uses
custom package identifiers, now receives those packages as.annual/.monthly.
3.0.0
Added
- Introductory offers.
SubscriptionPackage.introductoryOfferis anIntroductoryOffer: how it
is paid (.freeTrial,.payAsYouGo,.payUpFront), one period and how many, the price, and
whether this customer can take it (.eligible,.ineligible,.unknown). The eligibility is
asked of the store when the offering loads, and only for packages that have an offer.
isEligibleFreeTrialis the one question a paywall asks before writing "7 days free", and
regularBillingDate(from:in:)is the date the regular price is first charged, counted in
calendar units. Anything the store did not answer "eligible" is not eligible, so a customer
who has used the trial is never promised it. PaywallPlanRowshows a free trial as a caption under the plan title, from the new
PaywallLabels.freeTrial, only when the customer is eligible. The price stays the largest text.- Choosing an offering by identifier.
SubscriptionUseCase.loadOffering(id:)and
purchase(packageId:inOffering:);PaywallView(offering:)loads and sells from that offering.
An identifier the dashboard does not have finds nothing — the current offering is never sold in
its place. Both calls have default implementations that answer fromloadOfferings()and
purchase(packageId:), correct for a conformance that knows a single offering, so existing
stand-ins compile unchanged. PaywallHandlers.offeringIdentifier, set byPaywallContainerfrom its mode. Passing it to
PaywallViewkeeps the fallback to the app's own paywall selling the offering the mode named.
Until now the fallback always sold the current offering.SubscriptionError.purchasePending: a purchase waiting for approval (Ask to Buy). Until now it
arrived as.purchaseFailed.onPendingandonNothingToRestoreonPaywallView,RevenueCatPaywallView,
PaywallContainerandPaywallHandlers. With noonPending, a pending purchase goes to
onErroras.purchasePending, so it is never silent. A restore that finds nothing still does
not callonEntitled.CachedEntitlement.lastKnownStatus(gracePeriod:at:)is public, so a widget or another
extension reading the app'sEntitlementCacheapplies the same grace rule as the app.PaywallLabels.japanese, andPaywallLabels.englishFreeTrial/japaneseFreeTrialfor apps
that assemble their own labels.
Changed
- BREAKING.
SubscriptionErrorhas a new case,.purchasePending. Aswitchover it with no
defaultstops compiling.catchpatterns are unaffected. - BREAKING. New parameters, all defaulted, on
SubscriptionPackage.init,PaywallLabels.init,
PaywallHandlers.init, bothPaywallViewinitialisers,RevenueCatPaywallView.initand
PaywallContainer.init. Call sites keep compiling; the signatures changed.
Migrating from 2.x
- Raise the requirement to
from: "3.0.0". No call site needs to change unless it switches over
SubscriptionErrorexhaustively; add.purchasePendingthere, as an outcome, not an error. - A pending purchase used to reach
onErroras.purchaseFailed; it now reachesonPending, or
onErroras.purchasePendingwhen there is none. - To sell the offering a
PaywallContainermode names from the app's own paywall too, pass
offering: handlers.offeringIdentifiertoPaywallView.
2.1.0
Added
- A
SubscriptionRevenueCatUIproduct, the only one that links RevenueCatUI. It carries
RevenueCatPaywallView, which shows a paywall designed with RevenueCat Paywalls for an
offering (the current one fornil), andPaywallContainer, which shows either that or the
app's own paywall from one place. After a purchase or a restore the RevenueCat paywall asks the
SubscriptionUseCasewhether the customer is entitled, rather than reading theCustomerInfo
RevenueCatUI hands back, so the configured entitlement decides and theEntitlementCacheis
refreshed; a restore that finds nothing does not count as entitled. PaywallModeinSubscriptionUI:.custom,.revenueCat(offering:)and
.automatic(offering:), with.revenueCatand.automaticas shorthands for the current
offering. A struct rather than an enum, because an enum case with an associated value and a
static property of the same name makePaywallMode.automaticambiguous.PaywallMode.choice(for:)is the pure rule that turns the mode and a
PaywallOfferingLookupinto aPaywallChoice; everything that stops a RevenueCat paywall from
being shown falls back to the app's own. Held inSubscriptionUIso that an app can keep the
setting without linking RevenueCatUI.PaywallHandlers: theonEntitled/onError/onDismissthatPaywallContainerpasses
to the closure building the app's own paywall, so both paywalls report in the same three
callbacks.
Changed
- The minimum RevenueCat SDK is now 5.19.0, the first with
Offering.hasPaywall.
2.0.0
Added
- The entitlement now survives a launch.
EntitlementCacheis a place the app supplies, every
reading the store confirms is written to it, and a launch that cannot reach the store starts
from what is there instead of from "not subscribed".FileEntitlementCache,
UserDefaultsEntitlementCacheandInMemoryEntitlementCacheship with it; there is no
default, because the place belongs to the app.UserDefaults.standardis deliberately not
one of the answers — an extension reads a different suite, and parallel tests write over each
other's records. SubscriptionConfiguration.gracePeriod, three days by default: how long a cached entitlement
is honoured past its expiration date while the store is unreachable. It is never applied to a
reading the store answered, because the store runs a billing-retry grace of its own during
which it reports an active entitlement whose date has already passed.SubscriptionStatus.verification, anEntitlementVerificationof.confirmed(at:),
.lastKnown(at:)or.unverified. Replaying a cached entitlement is a rule, not a silent
fallback, so the caller can see which of the three it is holding — and can tell "not
subscribed" from "not known yet", which.inactivealone never distinguished.- A
SubscriptionUIproduct:PaywallViewand the parts it is built from
(PaywallPage,PaywallPlanPicker,PaywallPlanRow,PaywallLegalFooter,
PaywallLegalLinks,PaywallLabels), plusPreviewSubscriptionUseCase. It depends on
SwiftUI andSubscriptiononly — no design system, and no colour, typeface or background
choices. The billed amount is the largest text in every plan row and no parameter can change
that,PaywallLegalLinksrequires both the terms and the privacy URL, and several pages are
paged through with an index. Until now the onlyPaywallViewin this repository was a snippet
inside the DocC article.
Changed
- Breaking.
SubscriptionUseCaseImpl.init(configuration:)now also takes
entitlementCache:, with no default. Defaulting it toUserDefaults.standardwould put the
record where an extension cannot read it; defaulting it to the in-memory cache would keep the
old behaviour under a new name. Both hide the choice, so the choice is required. - Breaking.
SubscriptionStatus.initandSubscriptionConfiguration.inittake new
parameters. Both have defaults, so call sites keep compiling, but the signatures changed. getSubscriptionStatus()answers from the cache before the store has replied, so.inactive
early in a launch no longer means "nothing is known" by itself — readverificationfor that.observeSubscriptionStatus()stamps the statuses it yields, so a value taken from the stream
carries the same provenance as one returned bycheckSubscriptionStatus().syncUser(userId:)drops the cached record along with the in-memory status when the identity
differs from the one on record, and drops nothing when it is the same. The second half is what
lets a relaunch out of signal keep the entitlement this device already confirmed.clearUser()clears the cache as well, so a signed-out account's entitlement does not come
back on the next launch.- DocC is built and hosted for both targets.
Fixed
- A cold launch with no network reported every paying customer as unsubscribed. The entitlement
lived only in an in-memory actor whose initial value was.inactive, and
checkSubscriptionStatus()threw before it could write anything, so nothing ever replaced it.
Where this did not happen, the RevenueCat SDK's own disk cache was answering — not a guarantee
this package made. - The test gate is back in CI.
.github/workflows/tests.ymlwas removed on 2026-08-11, after
1.0.5 had been tagged, which left that release's "Workflows synced to the shared template
(tests, release-on-tag)" line describing something the repository no longer had. Every push
and pull request now resolves dependencies fresh and runsswift buildandswift test.
1.0.5
v1.0.4
v1.0.3
追加
- DocC ドキュメントの自動生成と GitHub Pages への公開機能を追加
- Swift DocC Plugin を依存関係に追加
- GitHub Actions ワークフローで自動的にドキュメントを生成・デプロイ
- README に完全なドキュメントへのリンクを追加 (https://no-problem-dev.github.io/swift-subscription/documentation/subscription/)
変更
- ドキュメントへのアクセシビリティを向上
v1.0.2
📝 Documentation Improvements
README Standardization
- Moved badges to right after title for better visual consistency
- Renamed '要件' to '必要要件' for terminology consistency across packages
- Removed 'トラブルシューティング' section to streamline documentation
- Aligned with package-wide documentation standards across all SwiftPackages
Benefits
- ✅ Improved consistency across all packages
- ✅ Better badge visibility and professional appearance
- ✅ Cleaner, more focused documentation
- ✅ Follows industry best practices for Swift package documentation
No code changes - documentation improvements only.