mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-09 22:35:56 +05:45
feat: Introducing billing information for PurchaseInformation in CustomerCenter (#5066)
* feat: Update billing information based on subscription type * chore: Make StoreProductDiscountType spi public * missing spy * add missing @_spi(Internal) to TestStoreProductDiscount * feat: Introduce copy changes for active subscription * Added Self.management * bring back preview * bring back onchange * use card * hide chevron * hide chevron * ViewBuilder * revert tuist * fix tests * remove testable * remove testable * default with productId * remove trailing comma * use localizations * w * check itout
This commit is contained in:
1 parent
f3d2b18c2f
commit
4087a9ca39
19 files changed
+665
-451
No files matched your search
@@ -582,6 +582,9 @@
|
||||
5733D00928CFA7A4008638D8 /* MockPaymentQueueWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5733D00828CFA7A4008638D8 /* MockPaymentQueueWrapper.swift */; };
|
||||
5733D01128D00354008638D8 /* PromotionalOfferTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5733D01028D00354008638D8 /* PromotionalOfferTests.swift */; };
|
||||
5736267B2D3E76C1003C9665 /* ProductPaidPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5736267A2D3E76C1003C9665 /* ProductPaidPrice.swift */; };
|
||||
5738AF522DCBDD4700B6EDC5 /* PurchaseInformationCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5738AF512DCBDD4100B6EDC5 /* PurchaseInformationCardView.swift */; };
|
||||
5738AF542DCBEF0D00B6EDC5 /* PurchaseInformation+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5738AF532DCBEF0700B6EDC5 /* PurchaseInformation+Mock.swift */; };
|
||||
5738AF562DCBF0E000B6EDC5 /* StoreProductDiscountType+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5738AF552DCBF0DD00B6EDC5 /* StoreProductDiscountType+Mock.swift */; };
|
||||
5738F46E278CAC520096D623 /* StoreTransactionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5738F46D278CAC520096D623 /* StoreTransactionTests.swift */; };
|
||||
5738F489278CC2500096D623 /* MockTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F591492726B9956C00D32E58 /* MockTransaction.swift */; };
|
||||
573A10D52800A7C800F976E5 /* SKErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 573A10D42800A7C800F976E5 /* SKErrorTests.swift */; };
|
||||
@@ -826,6 +829,7 @@
|
||||
57F3C10529B7B22E0004FD7E /* CustomerInfo+ActiveDates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F3C10429B7B22E0004FD7E /* CustomerInfo+ActiveDates.swift */; };
|
||||
57F3C10C29B7EAE90004FD7E /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E357D16038F07915D7825D /* MockUserDefaults.swift */; };
|
||||
57F7DAAB2D9E906200855622 /* RuntimeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F7DAAA2D9E906100855622 /* RuntimeUtils.swift */; };
|
||||
57F9725A2DC3CA5400C6E540 /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F972592DC3CA5200C6E540 /* Transaction.swift */; };
|
||||
57F9726B2DC3EF9500C6E540 /* MockStoreProductDiscount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57F9726A2DC3EF9500C6E540 /* MockStoreProductDiscount.swift */; };
|
||||
57FD7B1528DA4037009CA4E4 /* PurchasesType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FD7B1428DA4037009CA4E4 /* PurchasesType.swift */; };
|
||||
57FDAA962846BDE2009A48F1 /* PurchasesTransactionHandlingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FDAA952846BDE2009A48F1 /* PurchasesTransactionHandlingTests.swift */; };
|
||||
@@ -1105,7 +1109,6 @@
|
||||
B3F3E8DA277158FE0047A5B9 /* DNSChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F3E8D9277158FE0047A5B9 /* DNSChecker.swift */; };
|
||||
B3F8418F26F3A93400E560FB /* ErrorCodeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3F8418E26F3A93400E560FB /* ErrorCodeTests.swift */; };
|
||||
C3AD12BA2C6EA61F00A4F86F /* CompatibilityNavigationStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AD12B92C6EA61F00A4F86F /* CompatibilityNavigationStack.swift */; };
|
||||
C3AD12BC2C6EA69D00A4F86F /* SubscriptionDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AD12BB2C6EA69D00A4F86F /* SubscriptionDetailsView.swift */; };
|
||||
F516BD29282434070083480B /* StoreKit2StorefrontListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = F516BD28282434070083480B /* StoreKit2StorefrontListener.swift */; };
|
||||
F52CFAFC28290AE500E8ABC5 /* StoreKit2StorefrontListenerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F516BD322828FDD90083480B /* StoreKit2StorefrontListenerTests.swift */; };
|
||||
F530E4FF275646EF001AF6BD /* MacDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = F530E4FE275646EF001AF6BD /* MacDevice.swift */; };
|
||||
@@ -1965,6 +1968,9 @@
|
||||
5733D00828CFA7A4008638D8 /* MockPaymentQueueWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPaymentQueueWrapper.swift; sourceTree = "<group>"; };
|
||||
5733D01028D00354008638D8 /* PromotionalOfferTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromotionalOfferTests.swift; sourceTree = "<group>"; };
|
||||
5736267A2D3E76C1003C9665 /* ProductPaidPrice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductPaidPrice.swift; sourceTree = "<group>"; };
|
||||
5738AF512DCBDD4100B6EDC5 /* PurchaseInformationCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseInformationCardView.swift; sourceTree = "<group>"; };
|
||||
5738AF532DCBEF0700B6EDC5 /* PurchaseInformation+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PurchaseInformation+Mock.swift"; sourceTree = "<group>"; };
|
||||
5738AF552DCBF0DD00B6EDC5 /* StoreProductDiscountType+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoreProductDiscountType+Mock.swift"; sourceTree = "<group>"; };
|
||||
5738F46D278CAC520096D623 /* StoreTransactionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreTransactionTests.swift; sourceTree = "<group>"; };
|
||||
573A10D42800A7C800F976E5 /* SKErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SKErrorTests.swift; sourceTree = "<group>"; };
|
||||
573A10D82800ADCD00F976E5 /* StoreKitErrorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitErrorTests.swift; sourceTree = "<group>"; };
|
||||
@@ -2169,6 +2175,7 @@
|
||||
57F2C60B29784C11009EE527 /* SwiftVersionCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftVersionCheck.swift; sourceTree = "<group>"; };
|
||||
57F3C10429B7B22E0004FD7E /* CustomerInfo+ActiveDates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomerInfo+ActiveDates.swift"; sourceTree = "<group>"; };
|
||||
57F7DAAA2D9E906100855622 /* RuntimeUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeUtils.swift; sourceTree = "<group>"; };
|
||||
57F972592DC3CA5200C6E540 /* Transaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = "<group>"; };
|
||||
57F9726A2DC3EF9500C6E540 /* MockStoreProductDiscount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreProductDiscount.swift; sourceTree = "<group>"; };
|
||||
57FD7B1428DA4037009CA4E4 /* PurchasesType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesType.swift; sourceTree = "<group>"; };
|
||||
57FDAA952846BDE2009A48F1 /* PurchasesTransactionHandlingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchasesTransactionHandlingTests.swift; sourceTree = "<group>"; };
|
||||
@@ -2456,7 +2463,6 @@
|
||||
B3F3E8D9277158FE0047A5B9 /* DNSChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DNSChecker.swift; sourceTree = "<group>"; };
|
||||
B3F8418E26F3A93400E560FB /* ErrorCodeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorCodeTests.swift; sourceTree = "<group>"; };
|
||||
C3AD12B92C6EA61F00A4F86F /* CompatibilityNavigationStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibilityNavigationStack.swift; sourceTree = "<group>"; };
|
||||
C3AD12BB2C6EA69D00A4F86F /* SubscriptionDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionDetailsView.swift; sourceTree = "<group>"; };
|
||||
EFB3CBAA73855779FE828CE2 /* ProductsFetcherSK1.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsFetcherSK1.swift; sourceTree = "<group>"; };
|
||||
F516BD28282434070083480B /* StoreKit2StorefrontListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2StorefrontListener.swift; sourceTree = "<group>"; };
|
||||
F516BD322828FDD90083480B /* StoreKit2StorefrontListenerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKit2StorefrontListenerTests.swift; sourceTree = "<group>"; };
|
||||
@@ -2838,6 +2844,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2C8EC6AE2CCBD33E00D6CCF8 /* ButtonComponentTests.swift */,
|
||||
57D963A62DC94244006B8845 /* Transaction.swift */,
|
||||
);
|
||||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
@@ -3817,7 +3824,9 @@
|
||||
353756562C382C2800A1B8D6 /* Data */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
57D963A62DC94244006B8845 /* Transaction.swift */,
|
||||
5738AF552DCBF0DD00B6EDC5 /* StoreProductDiscountType+Mock.swift */,
|
||||
5738AF532DCBEF0700B6EDC5 /* PurchaseInformation+Mock.swift */,
|
||||
57F972592DC3CA5200C6E540 /* Transaction.swift */,
|
||||
3563EB932DCAA37A0061E058 /* EntitlementInfo+Extensions.swift */,
|
||||
357C43AE2D80510400B96769 /* CustomerCenterManagementOption.swift */,
|
||||
355FDB5A2D783FC400D20E65 /* CustomerCenterActionWrapper.swift */,
|
||||
@@ -3870,7 +3879,6 @@
|
||||
3537565E2C382C2800A1B8D6 /* RestorePurchasesAlert.swift */,
|
||||
3551E39C2C4A6A1400D27C25 /* TintedProgressView.swift */,
|
||||
C3AD12B92C6EA61F00A4F86F /* CompatibilityNavigationStack.swift */,
|
||||
C3AD12BB2C6EA69D00A4F86F /* SubscriptionDetailsView.swift */,
|
||||
77372D982C6F8C7B008E59D3 /* AppUpdateWarningView.swift */,
|
||||
352304892D0A0F2B003971A4 /* ErrorView.swift */,
|
||||
);
|
||||
@@ -4516,6 +4524,7 @@
|
||||
574D02632DC65D3C0040FADC /* ManageSubscriptions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5738AF512DCBDD4100B6EDC5 /* PurchaseInformationCardView.swift */,
|
||||
3537565C2C382C2800A1B8D6 /* ManageSubscriptionsView.swift */,
|
||||
3531DF872CFE138800D454BF /* ManageSubscriptionsButtonsView.swift */,
|
||||
);
|
||||
@@ -7039,6 +7048,7 @@
|
||||
88B1BAEE2C813A3C001B7EE5 /* TextComponentView.swift in Sources */,
|
||||
3546355F2C391F4D001D7E85 /* PromotionalOfferView.swift in Sources */,
|
||||
2C7457882CEDF7C0004ACE52 /* BackgroundStyle.swift in Sources */,
|
||||
5738AF542DCBEF0D00B6EDC5 /* PurchaseInformation+Mock.swift in Sources */,
|
||||
2C8EC6DD2CCC7C5B00D6CCF8 /* PackageValidator.swift in Sources */,
|
||||
574D025D2DC65C8B0040FADC /* ManageSubscriptionView.swift in Sources */,
|
||||
4DEB9BC52D08CA1700D33E36 /* BadgeModifier.swift in Sources */,
|
||||
@@ -7048,6 +7058,7 @@
|
||||
353756722C382C2800A1B8D6 /* URLUtilities.swift in Sources */,
|
||||
57D8D5532D3EAF11009CB6ED /* CompatibilityLabeledContent.swift in Sources */,
|
||||
03C06FCA2D479C7400600693 /* CarouselComponentView.swift in Sources */,
|
||||
5738AF562DCBF0E000B6EDC5 /* StoreProductDiscountType+Mock.swift in Sources */,
|
||||
353FDC0F2CA446FA0055F328 /* StoreProductDiscount+Extensions.swift in Sources */,
|
||||
03A98CF12D222F5F009BCA61 /* FallbackComponentPreview.swift in Sources */,
|
||||
887A60862C1D037000E1A461 /* FooterHidingModifier.swift in Sources */,
|
||||
@@ -7095,6 +7106,7 @@
|
||||
2C4C36132C6FBA8B00AE959B /* CompatibilityTopBarTrailing.swift in Sources */,
|
||||
FD9BCEBC2DC946C400408A5D /* BottomSheetView.swift in Sources */,
|
||||
3546355C2C391F38001D7E85 /* FeedbackSurveyViewModel.swift in Sources */,
|
||||
5738AF522DCBDD4700B6EDC5 /* PurchaseInformationCardView.swift in Sources */,
|
||||
353756692C382C2800A1B8D6 /* CustomerCenterViewState.swift in Sources */,
|
||||
7783607B2CCA88E4000785B8 /* StickyFooterComponentView.swift in Sources */,
|
||||
887A608B2C1D037000E1A461 /* PurchaseHandler+TestData.swift in Sources */,
|
||||
@@ -7103,8 +7115,8 @@
|
||||
887A60682C1D037000E1A461 /* TemplateError.swift in Sources */,
|
||||
887A60792C1D037000E1A461 /* UserInterfaceIdiom.swift in Sources */,
|
||||
03C06FCC2D479C7C00600693 /* CarouselComponentViewModel.swift in Sources */,
|
||||
57F9725A2DC3CA5400C6E540 /* Transaction.swift in Sources */,
|
||||
35D41B432D403556000621C7 /* Store+Localization.swift in Sources */,
|
||||
C3AD12BC2C6EA69D00A4F86F /* SubscriptionDetailsView.swift in Sources */,
|
||||
887A60742C1D037000E1A461 /* Strings.swift in Sources */,
|
||||
57233AA32D3F976500488B00 /* CustomerCenterLocalizationStrings.swift in Sources */,
|
||||
887A60712C1D037000E1A461 /* PaywallViewConfiguration.swift in Sources */,
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RevenueCat
|
||||
@_spi(Internal) import RevenueCat
|
||||
|
||||
// swiftlint:disable force_unwrapping
|
||||
extension CustomerCenterConfigData {
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
@@ -138,75 +137,6 @@ extension CustomerCenterConfigData {
|
||||
@available(iOS 14.0, *)
|
||||
static let `default` = mock(lastPublishedAppVersion: "1.0.0")
|
||||
|
||||
static let subscriptionInformationMonthlyRenewing: PurchaseInformation = .init(
|
||||
title: "Basic",
|
||||
durationTitle: "Monthly",
|
||||
explanation: .earliestRenewal,
|
||||
price: .paid("$4.99"),
|
||||
expirationOrRenewal: .init(label: .nextBillingDate,
|
||||
date: .date("June 1st, 2024")),
|
||||
productIdentifier: "product_id",
|
||||
store: .appStore,
|
||||
isLifetime: false,
|
||||
isTrial: false,
|
||||
isCancelled: false,
|
||||
latestPurchaseDate: nil,
|
||||
customerInfoRequestedDate: Date(),
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
)
|
||||
|
||||
static let subscriptionInformationFree: PurchaseInformation = .init(
|
||||
title: "Basic",
|
||||
durationTitle: "Monthly",
|
||||
explanation: .earliestRenewal,
|
||||
price: .free,
|
||||
expirationOrRenewal: .init(label: .nextBillingDate,
|
||||
date: .date("June 1st, 2024")),
|
||||
productIdentifier: "product_id",
|
||||
store: .appStore,
|
||||
isLifetime: false,
|
||||
isTrial: false,
|
||||
isCancelled: false,
|
||||
latestPurchaseDate: nil,
|
||||
customerInfoRequestedDate: Date(),
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
)
|
||||
|
||||
static func subscriptionInformationYearlyExpiring(store: Store = .appStore) -> PurchaseInformation {
|
||||
PurchaseInformation(
|
||||
title: "Basic",
|
||||
durationTitle: "Yearly",
|
||||
explanation: .earliestRenewal,
|
||||
price: .paid("$49.99"),
|
||||
expirationOrRenewal: .init(label: .expires,
|
||||
date: .date("June 1st, 2024")),
|
||||
productIdentifier: "product_id",
|
||||
store: store,
|
||||
isLifetime: false,
|
||||
isTrial: false,
|
||||
isCancelled: false,
|
||||
latestPurchaseDate: nil,
|
||||
customerInfoRequestedDate: Date(),
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
)
|
||||
}
|
||||
|
||||
static let consumable: PurchaseInformation = .init(
|
||||
title: "Basic",
|
||||
durationTitle: nil,
|
||||
explanation: .lifetime,
|
||||
price: .paid("$49.99"),
|
||||
expirationOrRenewal: nil,
|
||||
productIdentifier: "product_id",
|
||||
store: .appStore,
|
||||
isLifetime: true,
|
||||
isTrial: false,
|
||||
isCancelled: false,
|
||||
latestPurchaseDate: Date(),
|
||||
customerInfoRequestedDate: Date(),
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
)
|
||||
|
||||
static let standardAppearance = CustomerCenterConfigData.Appearance(
|
||||
accentColor: .init(light: "#007AFF", dark: "#007AFF"),
|
||||
textColor: .init(light: "#000000", dark: "#ffffff"),
|
||||
@@ -214,5 +144,4 @@ extension CustomerCenterConfigData {
|
||||
buttonTextColor: .init(light: "#ffffff", dark: "#000000"),
|
||||
buttonBackgroundColor: .init(light: "#287aff", dark: "#287aff")
|
||||
)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// Copyright RevenueCat Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://opensource.org/licenses/MIT
|
||||
//
|
||||
// PurchaseInformation+Mock.swift
|
||||
//
|
||||
// Created by Facundo Menzella on 7/5/25.
|
||||
|
||||
import Foundation
|
||||
@_spi(Internal) import RevenueCat
|
||||
|
||||
// swiftlint:disable force_unwrapping
|
||||
extension PurchaseInformation {
|
||||
static let monthlyRenewing = PurchaseInformation(
|
||||
title: "Basic",
|
||||
durationTitle: "Monthly",
|
||||
explanation: .earliestRenewal,
|
||||
price: .paid("$4.99"),
|
||||
expirationOrRenewal: .init(label: .nextBillingDate, date: .date("June 1st, 2024")),
|
||||
productIdentifier: "product_id5",
|
||||
store: .appStore,
|
||||
isLifetime: false,
|
||||
isTrial: false,
|
||||
latestPurchaseDate: nil,
|
||||
customerInfoRequestedDate: Date(),
|
||||
isCancelled: false,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
)
|
||||
|
||||
static let subscriptionInformationFree = PurchaseInformation(
|
||||
title: "Basic",
|
||||
durationTitle: "Monthly",
|
||||
explanation: .earliestRenewal,
|
||||
price: .free,
|
||||
expirationOrRenewal: .init(label: .nextBillingDate,
|
||||
date: .date("June 1st, 2024")),
|
||||
productIdentifier: "product_id2",
|
||||
store: .appStore,
|
||||
isLifetime: false,
|
||||
isTrial: false,
|
||||
latestPurchaseDate: nil,
|
||||
customerInfoRequestedDate: Date(),
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
)
|
||||
|
||||
static func yearlyExpiring(
|
||||
title: String = "Product name",
|
||||
productIdentifier: String = "productIdentifier3",
|
||||
store: Store = .appStore,
|
||||
isCancelled: Bool = false,
|
||||
expirationDate: Date = Date(),
|
||||
introductoryDiscount: StoreProductDiscountType? = nil
|
||||
) -> PurchaseInformation {
|
||||
PurchaseInformation(
|
||||
title: title,
|
||||
durationTitle: "Yearly",
|
||||
explanation: .earliestRenewal,
|
||||
price: .paid("$49.99"),
|
||||
expirationOrRenewal: .init(label: .expires, date: .date("June 1st, 2024")),
|
||||
productIdentifier: productIdentifier,
|
||||
store: store,
|
||||
isLifetime: false,
|
||||
isTrial: false,
|
||||
latestPurchaseDate: nil,
|
||||
customerInfoRequestedDate: Date(),
|
||||
isCancelled: isCancelled,
|
||||
introductoryDiscount: introductoryDiscount,
|
||||
expirationDate: expirationDate,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
)
|
||||
}
|
||||
|
||||
static let consumable: PurchaseInformation = PurchaseInformation(
|
||||
title: "Basic",
|
||||
durationTitle: nil,
|
||||
explanation: .lifetime,
|
||||
price: .paid("$49.99"),
|
||||
expirationOrRenewal: nil,
|
||||
productIdentifier: "product_id",
|
||||
store: .appStore,
|
||||
isLifetime: true,
|
||||
isTrial: false,
|
||||
latestPurchaseDate: Date(),
|
||||
customerInfoRequestedDate: Date(),
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import Foundation
|
||||
@_spi(Internal) import RevenueCat
|
||||
import StoreKit
|
||||
|
||||
// swiftlint:disable nesting
|
||||
// swiftlint:disable nesting file_length
|
||||
|
||||
/// Information about a purchase.
|
||||
struct PurchaseInformation {
|
||||
@@ -54,7 +54,7 @@ struct PurchaseInformation {
|
||||
/// - `false` for purchases outside the trial period.
|
||||
let isTrial: Bool
|
||||
|
||||
/// Indicates whether the purchased subscription is cancelled
|
||||
/// Indicates wheter the purchased subscripcion is cancelled
|
||||
/// - `true` if the subscription is user-cancelled
|
||||
/// - `false` if the subscription is not user-cancelled
|
||||
///
|
||||
@@ -62,8 +62,18 @@ struct PurchaseInformation {
|
||||
let isCancelled: Bool
|
||||
|
||||
let latestPurchaseDate: Date?
|
||||
|
||||
/// The fetch date of this CustomerInfo. (a.k.a. CustomerInfo.requestedDate)
|
||||
let customerInfoRequestedDate: Date
|
||||
|
||||
let introductoryDiscount: StoreProductDiscountType?
|
||||
|
||||
let expirationDate: Date?
|
||||
|
||||
let renewalDate: Date?
|
||||
|
||||
private let dateFormatter: DateFormatter
|
||||
|
||||
let managePurchaseURL: URL?
|
||||
|
||||
init(title: String,
|
||||
@@ -75,9 +85,12 @@ struct PurchaseInformation {
|
||||
store: Store,
|
||||
isLifetime: Bool,
|
||||
isTrial: Bool,
|
||||
isCancelled: Bool,
|
||||
latestPurchaseDate: Date?,
|
||||
customerInfoRequestedDate: Date,
|
||||
isCancelled: Bool = false,
|
||||
introductoryDiscount: StoreProductDiscountType? = nil,
|
||||
expirationDate: Date? = nil,
|
||||
renewalDate: Date? = nil,
|
||||
managePurchaseURL: URL?
|
||||
) {
|
||||
self.title = title
|
||||
@@ -92,23 +105,29 @@ struct PurchaseInformation {
|
||||
self.isCancelled = isCancelled
|
||||
self.latestPurchaseDate = latestPurchaseDate
|
||||
self.customerInfoRequestedDate = customerInfoRequestedDate
|
||||
self.introductoryDiscount = introductoryDiscount
|
||||
self.expirationDate = expirationDate
|
||||
self.renewalDate = renewalDate
|
||||
self.dateFormatter = Self.defaultDateFormatter
|
||||
self.managePurchaseURL = managePurchaseURL
|
||||
}
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
init(entitlement: EntitlementInfo? = nil,
|
||||
subscribedProduct: StoreProduct? = nil,
|
||||
storeProduct: StoreProduct? = nil,
|
||||
transaction: Transaction,
|
||||
renewalPrice: PriceDetails? = nil,
|
||||
customerInfoRequestedDate: Date,
|
||||
dateFormatter: DateFormatter = DateFormatter(),
|
||||
managePurchaseURL: URL?) {
|
||||
managePurchaseURL: URL?,
|
||||
dateFormatter: DateFormatter = Self.defaultDateFormatter
|
||||
) {
|
||||
dateFormatter.dateStyle = .medium
|
||||
|
||||
// Title and duration from product if available
|
||||
self.title = subscribedProduct?.localizedTitle
|
||||
self.durationTitle = subscribedProduct?.subscriptionPeriod?.durationTitle
|
||||
self.title = storeProduct?.localizedTitle
|
||||
self.durationTitle = storeProduct?.subscriptionPeriod?.durationTitle
|
||||
self.customerInfoRequestedDate = customerInfoRequestedDate
|
||||
self.introductoryDiscount = storeProduct?.introductoryDiscount
|
||||
self.managePurchaseURL = managePurchaseURL
|
||||
|
||||
// Use entitlement data if available, otherwise derive from transaction
|
||||
@@ -120,12 +139,15 @@ struct PurchaseInformation {
|
||||
if let renewalPrice {
|
||||
self.price = renewalPrice
|
||||
} else {
|
||||
self.price = entitlement.priceBestEffort(product: subscribedProduct)
|
||||
self.price = entitlement.priceBestEffort(product: storeProduct)
|
||||
}
|
||||
self.isLifetime = entitlement.expirationDate == nil
|
||||
|
||||
self.isTrial = entitlement.periodType == .trial
|
||||
self.isCancelled = entitlement.isCancelled
|
||||
self.latestPurchaseDate = entitlement.latestPurchaseDate
|
||||
self.expirationDate = entitlement.expirationDate
|
||||
self.renewalDate = entitlement.willRenew ? entitlement.expirationDate : nil
|
||||
} else {
|
||||
switch transaction.type {
|
||||
case let .subscription(isActive, willRenew, expiresDate, isTrial):
|
||||
@@ -141,12 +163,17 @@ struct PurchaseInformation {
|
||||
}
|
||||
self.isLifetime = false
|
||||
self.isTrial = isTrial
|
||||
|
||||
self.expirationDate = expiresDate
|
||||
self.renewalDate = willRenew ? expiresDate : nil
|
||||
self.latestPurchaseDate = (transaction as? RevenueCat.SubscriptionInfo)?.purchaseDate
|
||||
|
||||
case .nonSubscription:
|
||||
self.explanation = .lifetime
|
||||
self.expirationOrRenewal = nil
|
||||
self.isLifetime = true
|
||||
self.renewalDate = nil
|
||||
self.expirationDate = nil
|
||||
self.isTrial = false
|
||||
self.latestPurchaseDate = (transaction as? NonSubscriptionTransaction)?.purchaseDate
|
||||
}
|
||||
@@ -161,10 +188,12 @@ struct PurchaseInformation {
|
||||
if let renewalPrice {
|
||||
self.price = renewalPrice
|
||||
} else {
|
||||
self.price = subscribedProduct.map { .paid($0.localizedPriceString) } ?? .unknown
|
||||
self.price = storeProduct.map { .paid($0.localizedPriceString) } ?? .unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.dateFormatter = dateFormatter
|
||||
}
|
||||
|
||||
struct ExpirationOrRenewal: Equatable {
|
||||
@@ -202,6 +231,11 @@ struct PurchaseInformation {
|
||||
case lifetime
|
||||
}
|
||||
|
||||
private static let defaultDateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
return dateFormatter
|
||||
}()
|
||||
}
|
||||
// swiftlint:enable nesting
|
||||
|
||||
@@ -222,19 +256,19 @@ extension PurchaseInformation {
|
||||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, visionOS 1.0, *)
|
||||
static func purchaseInformationUsingRenewalInfo(
|
||||
entitlement: EntitlementInfo? = nil,
|
||||
subscribedProduct: StoreProduct,
|
||||
storeProduct: StoreProduct,
|
||||
transaction: Transaction,
|
||||
customerCenterStoreKitUtilities: CustomerCenterStoreKitUtilitiesType,
|
||||
customerInfoRequestedDate: Date,
|
||||
managePurchaseURL: URL?
|
||||
) async -> PurchaseInformation {
|
||||
let renewalPriceDetails = await Self.extractPriceDetailsFromRenewalInfo(
|
||||
forProduct: subscribedProduct,
|
||||
forProduct: storeProduct,
|
||||
customerCenterStoreKitUtilities: customerCenterStoreKitUtilities
|
||||
)
|
||||
return PurchaseInformation(
|
||||
entitlement: entitlement,
|
||||
subscribedProduct: subscribedProduct,
|
||||
storeProduct: storeProduct,
|
||||
transaction: transaction,
|
||||
renewalPrice: renewalPriceDetails,
|
||||
customerInfoRequestedDate: customerInfoRequestedDate,
|
||||
@@ -273,8 +307,6 @@ extension PurchaseInformation: Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
extension PurchaseInformation: Equatable { }
|
||||
|
||||
private extension EntitlementInfo {
|
||||
|
||||
var isCancelled: Bool {
|
||||
@@ -376,5 +408,67 @@ private extension String {
|
||||
func isPromotionalLifetime(store: Store) -> Bool {
|
||||
return self.hasSuffix("_lifetime") && store == .promotional
|
||||
}
|
||||
}
|
||||
|
||||
extension PurchaseInformation {
|
||||
|
||||
func billingInformation(localizations: CustomerCenterConfigData.Localization) -> String {
|
||||
guard let expirationDate else {
|
||||
// non subscription
|
||||
return localizations[.pricePaid]
|
||||
.replacingOccurrences(of: "{{ price }}", with: price.billingInformation(localizations: localizations))
|
||||
}
|
||||
|
||||
if let introductoryDiscount {
|
||||
if isCancelled {
|
||||
var renewString = "\(introductoryDiscount.localizedPricePerPeriodByPaymentMode(.current))."
|
||||
renewString += localizations[.expiresOnDateWithoutChanges]
|
||||
.replacingOccurrences(of: "{{ date }}", with: dateFormatter.string(from: expirationDate))
|
||||
return renewString
|
||||
}
|
||||
|
||||
if introductoryDiscount.paymentMode == .freeTrial {
|
||||
return localizations[.freeTrialUntilDate]
|
||||
.replacingOccurrences(of: "{{ date }}", with: dateFormatter.string(from: expirationDate))
|
||||
} else {
|
||||
let introPrice = introductoryDiscount.localizedPricePerPeriodByPaymentMode(.current)
|
||||
return "\(introPrice). \(priceAfterDiscount(localizations: localizations))"
|
||||
}
|
||||
} else if isCancelled {
|
||||
return localizations[.expiresOnDateWithoutChanges]
|
||||
.replacingOccurrences(of: "{{ date }}", with: dateFormatter.string(from: expirationDate))
|
||||
} else {
|
||||
switch price {
|
||||
case let .paid(priceString):
|
||||
return localizations[.renewsOnDateForPrice]
|
||||
.replacingOccurrences(of: "{{ date }}", with: dateFormatter.string(from: expirationDate))
|
||||
.replacingOccurrences(of: "{{ price }}", with: priceString)
|
||||
case .free, .unknown:
|
||||
return localizations[.renewsOnDate]
|
||||
.replacingOccurrences(of: "{{ date }}", with: dateFormatter.string(from: expirationDate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func priceAfterDiscount(localizations: CustomerCenterConfigData.Localization) -> String {
|
||||
return localizations[.priceAfterwards]
|
||||
.replacingOccurrences(of: "{{ price }}", with: price.billingInformation(localizations: localizations))
|
||||
}
|
||||
}
|
||||
|
||||
private extension PurchaseInformation.PriceDetails {
|
||||
func billingInformation(localizations: CustomerCenterConfigData.Localization) -> String {
|
||||
switch self {
|
||||
case .free:
|
||||
return localizations[.free]
|
||||
case .paid(let priceString):
|
||||
return priceString
|
||||
case .unknown:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
var isCancelled: Bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// Copyright RevenueCat Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://opensource.org/licenses/MIT
|
||||
//
|
||||
// StoreProductDiscountType+Mock.swift
|
||||
//
|
||||
// Created by Facundo Menzella on 7/5/25.
|
||||
|
||||
import Foundation
|
||||
@_spi(Internal) import RevenueCat
|
||||
|
||||
@_spi(Internal) extension StoreProductDiscountType {
|
||||
|
||||
static func discount(
|
||||
paymentMode: StoreProductDiscount.PaymentMode = .payAsYouGo,
|
||||
price: Decimal = 0.99,
|
||||
subscriptionPeriod: SubscriptionPeriod = .init(value: 1, unit: .month),
|
||||
numberOfPeriods: Int = 3,
|
||||
type: StoreProductDiscount.DiscountType = .introductory
|
||||
) -> any StoreProductDiscountType {
|
||||
MockStoreProductDiscount(
|
||||
offerIdentifier: "offerIdentifier",
|
||||
currencyCode: "USD",
|
||||
price: price,
|
||||
localizedPriceString: "\(price) USD",
|
||||
paymentMode: paymentMode,
|
||||
subscriptionPeriod: subscriptionPeriod,
|
||||
numberOfPeriods: numberOfPeriods,
|
||||
type: type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct MockStoreProductDiscount: StoreProductDiscountType {
|
||||
|
||||
let offerIdentifier: String?
|
||||
let currencyCode: String?
|
||||
let price: Decimal
|
||||
let localizedPriceString: String
|
||||
let paymentMode: StoreProductDiscount.PaymentMode
|
||||
let subscriptionPeriod: RevenueCat.SubscriptionPeriod
|
||||
let numberOfPeriods: Int
|
||||
let type: StoreProductDiscount.DiscountType
|
||||
|
||||
static func mock(
|
||||
paymentMode: StoreProductDiscount.PaymentMode = .payAsYouGo,
|
||||
price: Decimal = 0.99,
|
||||
subscriptionPeriod: SubscriptionPeriod = .init(value: 1, unit: .month),
|
||||
numberOfPeriods: Int = 3,
|
||||
discountType: StoreProductDiscount.DiscountType
|
||||
) -> MockStoreProductDiscount {
|
||||
MockStoreProductDiscount(
|
||||
offerIdentifier: nil,
|
||||
currencyCode: nil,
|
||||
price: 0.01,
|
||||
localizedPriceString: "$0.01",
|
||||
paymentMode: paymentMode,
|
||||
subscriptionPeriod: SubscriptionPeriod(value: 1, unit: .month),
|
||||
numberOfPeriods: 6,
|
||||
type: discountType
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ extension CustomerInfo {
|
||||
/// - Note: This is a **temporary** implementation and should eventually be replaced by
|
||||
/// backend-side logic for consistency and accuracy.
|
||||
///
|
||||
func earliestExpiringTransaction() -> Transaction? {
|
||||
func earliestExpiringTransaction() -> RevenueCatUI.Transaction? {
|
||||
let activeSubscriptions = subscriptionsByProductIdentifier.values
|
||||
.filter(\.isActive)
|
||||
.sorted(by: {
|
||||
|
||||
@@ -235,14 +235,14 @@ private extension CustomerCenterViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
func createPurchaseInformation(for transaction: Transaction,
|
||||
func createPurchaseInformation(for transaction: RevenueCatUI.Transaction,
|
||||
entitlement: EntitlementInfo?,
|
||||
customerInfo: CustomerInfo) async throws -> PurchaseInformation {
|
||||
if transaction.store == .appStore {
|
||||
if let product = await purchasesProvider.products([transaction.productIdentifier]).first {
|
||||
return await PurchaseInformation.purchaseInformationUsingRenewalInfo(
|
||||
entitlement: entitlement,
|
||||
subscribedProduct: product,
|
||||
storeProduct: product,
|
||||
transaction: transaction,
|
||||
customerCenterStoreKitUtilities: customerCenterStoreKitUtilities,
|
||||
customerInfoRequestedDate: customerInfo.requestDate,
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@ final class ManageSubscriptionsViewModel: BaseManageSubscriptionViewModel {
|
||||
activePurchases: [PurchaseInformation] = [],
|
||||
refundRequestStatus: RefundRequestStatus? = nil,
|
||||
purchasesProvider: CustomerCenterPurchasesType,
|
||||
loadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType? = nil) {
|
||||
loadPromotionalOfferUseCase: LoadPromotionalOfferUseCaseType? = nil
|
||||
) {
|
||||
self.activePurchases = activePurchases
|
||||
super.init(
|
||||
screen: screen,
|
||||
|
||||
@@ -66,8 +66,8 @@ import SwiftUI
|
||||
@available(watchOS, unavailable)
|
||||
struct CompatibilityLabeledContent<Label: View, Content: View>: View {
|
||||
|
||||
let label: () -> Label
|
||||
let content: () -> Content
|
||||
@ViewBuilder let label: () -> Label
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16.0, *) {
|
||||
|
||||
@@ -279,7 +279,7 @@ struct CustomerCenterView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
let purchaseInformationApple =
|
||||
CustomerCenterConfigData.subscriptionInformationMonthlyRenewing
|
||||
PurchaseInformation.monthlyRenewing
|
||||
let viewModelApple = CustomerCenterViewModel(
|
||||
purchaseInformation: purchaseInformationApple,
|
||||
configuration: CustomerCenterConfigData.default
|
||||
|
||||
@@ -137,9 +137,10 @@ struct ManageSubscriptionView: View {
|
||||
var content: some View {
|
||||
List {
|
||||
if let purchaseInformation = self.viewModel.purchaseInformation {
|
||||
SubscriptionDetailsView(
|
||||
PurchaseInformationCardView(
|
||||
purchaseInformation: purchaseInformation,
|
||||
refundRequestStatus: self.viewModel.refundRequestStatus
|
||||
showChevron: false,
|
||||
localization: localization
|
||||
)
|
||||
|
||||
Section {
|
||||
@@ -234,43 +235,103 @@ struct ManageSubscriptionView: View {
|
||||
viewModel: ManageSubscriptionViewModel(
|
||||
screen: CustomerCenterConfigData.default.screens[.management]!,
|
||||
showPurchaseHistory: false,
|
||||
purchaseInformation: CustomerCenterConfigData.subscriptionInformationMonthlyRenewing
|
||||
purchaseInformation: .monthlyRenewing
|
||||
)
|
||||
)
|
||||
}
|
||||
.preferredColorScheme(colorScheme)
|
||||
.previewDisplayName("Monthly renewing - \(colorScheme)")
|
||||
.previewDisplayName("Renewing subscription - No refund - No discount - \(colorScheme)")
|
||||
|
||||
CompatibilityNavigationStack {
|
||||
ManageSubscriptionView(
|
||||
viewModel: ManageSubscriptionViewModel(
|
||||
screen: CustomerCenterConfigData.default.screens[.management]!,
|
||||
showPurchaseHistory: false,
|
||||
purchaseInformation: CustomerCenterConfigData.subscriptionInformationYearlyExpiring()
|
||||
purchaseInformation: PurchaseInformation.yearlyExpiring()
|
||||
)
|
||||
)
|
||||
}
|
||||
.preferredColorScheme(colorScheme)
|
||||
.previewDisplayName("Yearly expiring - \(colorScheme)")
|
||||
.previewDisplayName("Renewing subscription - No refund - Pay as you go - \(colorScheme)")
|
||||
|
||||
CompatibilityNavigationStack {
|
||||
ManageSubscriptionView(
|
||||
viewModel: ManageSubscriptionViewModel(
|
||||
screen: CustomerCenterConfigData.default.screens[.management]!,
|
||||
showPurchaseHistory: false,
|
||||
purchaseInformation: CustomerCenterConfigData.subscriptionInformationFree
|
||||
purchaseInformation: .yearlyExpiring(
|
||||
introductoryDiscount: MockStoreProductDiscount.discount(
|
||||
paymentMode: .payUpFront,
|
||||
price: 2.99,
|
||||
subscriptionPeriod: .init(value: 3, unit: .month)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
.preferredColorScheme(colorScheme)
|
||||
.previewDisplayName("Free subscription - \(colorScheme)")
|
||||
.previewDisplayName("Renewing subscription - No refund - Pay up front - \(colorScheme)")
|
||||
|
||||
CompatibilityNavigationStack {
|
||||
ManageSubscriptionView(
|
||||
viewModel: ManageSubscriptionViewModel(
|
||||
screen: CustomerCenterConfigData.default.screens[.management]!,
|
||||
showPurchaseHistory: false,
|
||||
purchaseInformation: CustomerCenterConfigData.consumable
|
||||
purchaseInformation: .yearlyExpiring(
|
||||
introductoryDiscount: MockStoreProductDiscount.discount(
|
||||
paymentMode: .freeTrial,
|
||||
price: 2.99,
|
||||
subscriptionPeriod: .init(value: 3, unit: .month)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
.preferredColorScheme(colorScheme)
|
||||
.previewDisplayName("Renewing subscription - No refund - Free trial - \(colorScheme)")
|
||||
|
||||
CompatibilityNavigationStack {
|
||||
ManageSubscriptionView(
|
||||
viewModel: ManageSubscriptionViewModel(
|
||||
screen: CustomerCenterConfigData.default.screens[.management]!,
|
||||
showPurchaseHistory: false,
|
||||
purchaseInformation: .yearlyExpiring()
|
||||
)
|
||||
)
|
||||
}
|
||||
.preferredColorScheme(colorScheme)
|
||||
.previewDisplayName("Renewing subscription - Requested refund - No discount - \(colorScheme)")
|
||||
|
||||
CompatibilityNavigationStack {
|
||||
ManageSubscriptionView(
|
||||
viewModel: ManageSubscriptionViewModel(
|
||||
screen: CustomerCenterConfigData.default.screens[.management]!,
|
||||
showPurchaseHistory: true,
|
||||
purchaseInformation: .yearlyExpiring(isCancelled: true)
|
||||
)
|
||||
)
|
||||
}
|
||||
.preferredColorScheme(colorScheme)
|
||||
.previewDisplayName("Cancelled subscription - No refund - No discount - \(colorScheme)")
|
||||
|
||||
CompatibilityNavigationStack {
|
||||
ManageSubscriptionView(
|
||||
viewModel: ManageSubscriptionViewModel(
|
||||
screen: CustomerCenterConfigData.default.screens[.management]!,
|
||||
showPurchaseHistory: false,
|
||||
purchaseInformation: PurchaseInformation.subscriptionInformationFree
|
||||
)
|
||||
)
|
||||
}
|
||||
.preferredColorScheme(colorScheme)
|
||||
.previewDisplayName("Free subscription - No refund - No discount - \(colorScheme)")
|
||||
|
||||
CompatibilityNavigationStack {
|
||||
ManageSubscriptionView(
|
||||
viewModel: ManageSubscriptionViewModel(
|
||||
screen: CustomerCenterConfigData.default.screens[.management]!,
|
||||
showPurchaseHistory: false,
|
||||
purchaseInformation: PurchaseInformation.consumable
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -282,7 +343,7 @@ struct ManageSubscriptionView: View {
|
||||
viewModel: ManageSubscriptionViewModel(
|
||||
screen: CustomerCenterConfigData.default.screens[.management]!,
|
||||
showPurchaseHistory: false,
|
||||
purchaseInformation: CustomerCenterConfigData.subscriptionInformationYearlyExpiring(
|
||||
purchaseInformation: PurchaseInformation.yearlyExpiring(
|
||||
store: .playStore)
|
||||
)
|
||||
)
|
||||
@@ -293,8 +354,7 @@ struct ManageSubscriptionView: View {
|
||||
.environment(\.localization, CustomerCenterConfigData.default.localization)
|
||||
.environment(\.appearance, CustomerCenterConfigData.default.appearance)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
+99
-63
@@ -13,7 +13,7 @@
|
||||
// Created by Andrés Boedo on 5/3/24.
|
||||
//
|
||||
|
||||
import RevenueCat
|
||||
@_spi(Internal) import RevenueCat
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS)
|
||||
@@ -48,7 +48,8 @@ struct ManageSubscriptionsView: View {
|
||||
init(screen: CustomerCenterConfigData.Screen,
|
||||
activePurchases: Binding<[PurchaseInformation]>,
|
||||
purchasesProvider: CustomerCenterPurchasesType,
|
||||
actionWrapper: CustomerCenterActionWrapper) {
|
||||
actionWrapper: CustomerCenterActionWrapper
|
||||
) {
|
||||
let viewModel = ManageSubscriptionsViewModel(
|
||||
screen: screen,
|
||||
actionWrapper: actionWrapper,
|
||||
@@ -98,90 +99,115 @@ struct ManageSubscriptionsView: View {
|
||||
.environment(\.localization, localization)
|
||||
.environment(\.navigationOptions, navigationOptions)
|
||||
}
|
||||
.onChangeOf(activePurchases) { _ in
|
||||
.onChangeOf(activePurchases.first?.customerInfoRequestedDate) { _ in
|
||||
viewModel.updatePurchases(activePurchases)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var content: some View {
|
||||
List {
|
||||
if viewModel.purchasesMightBeDuplicated, support?.shouldWarnCustomersAboutMultipleSubscriptions == true {
|
||||
CompatibilityContentUnavailableView(
|
||||
localization[.youMayHaveDuplicatedSubscriptionsTitle],
|
||||
systemImage: "exclamationmark.square",
|
||||
description: Text(
|
||||
localization[.youMayHaveDuplicatedSubscriptionsSubtitle]
|
||||
)
|
||||
)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(
|
||||
Color(colorScheme == .light
|
||||
? UIColor.systemBackground
|
||||
: UIColor.secondarySystemBackground)
|
||||
ZStack {
|
||||
Color(
|
||||
colorScheme == .dark
|
||||
? UIColor.secondarySystemBackground
|
||||
: UIColor.systemGroupedBackground
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 16) {
|
||||
if viewModel.purchasesMightBeDuplicated,
|
||||
support?.shouldWarnCustomersAboutMultipleSubscriptions == true {
|
||||
CompatibilityContentUnavailableView(
|
||||
localization[.youMayHaveDuplicatedSubscriptionsTitle],
|
||||
systemImage: "exclamationmark.square",
|
||||
description: Text(
|
||||
localization[.youMayHaveDuplicatedSubscriptionsSubtitle]
|
||||
)
|
||||
)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding(.horizontal, 24)
|
||||
.padding(.top, 24)
|
||||
)
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(
|
||||
Color(colorScheme == .light
|
||||
? UIColor.systemBackground
|
||||
: UIColor.secondarySystemBackground)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if viewModel.activePurchases.isEmpty {
|
||||
let fallbackDescription = localization[.tryCheckRestore]
|
||||
if viewModel.activePurchases.isEmpty {
|
||||
let fallbackDescription = localization[.tryCheckRestore]
|
||||
|
||||
Section {
|
||||
CompatibilityContentUnavailableView(
|
||||
self.viewModel.screen.title,
|
||||
systemImage: "exclamationmark.triangle.fill",
|
||||
description: Text(self.viewModel.screen.subtitle ?? fallbackDescription)
|
||||
)
|
||||
}
|
||||
CompatibilityContentUnavailableView(
|
||||
self.viewModel.screen.title,
|
||||
systemImage: "exclamationmark.triangle.fill",
|
||||
description: Text(self.viewModel.screen.subtitle ?? fallbackDescription)
|
||||
)
|
||||
.padding(.horizontal)
|
||||
|
||||
Section {
|
||||
ManageSubscriptionsButtonsView(
|
||||
viewModel: self.viewModel
|
||||
)
|
||||
}
|
||||
ManageSubscriptionsButtonsView(
|
||||
viewModel: self.viewModel
|
||||
)
|
||||
.padding(.horizontal)
|
||||
|
||||
} else {
|
||||
Section(localization[.activeSubscriptions]) {
|
||||
ForEach(viewModel.activePurchases) { purchase in
|
||||
Button {
|
||||
viewModel.purchaseInformation = purchase
|
||||
} label: {
|
||||
CompatibilityLabeledContent {
|
||||
if purchase.title?.isEmpty == true {
|
||||
Text(purchase.productIdentifier)
|
||||
} else {
|
||||
Text(purchase.title ?? "")
|
||||
}
|
||||
} content: {
|
||||
Image(systemName: "chevron.forward")
|
||||
} else {
|
||||
Text(localization[.activeSubscriptions].uppercased())
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
ForEach(viewModel.activePurchases) { purchase in
|
||||
Button {
|
||||
viewModel.purchaseInformation = purchase
|
||||
} label: {
|
||||
PurchaseInformationCardView(
|
||||
purchaseInformation: purchase,
|
||||
localization: localization
|
||||
)
|
||||
.padding(16)
|
||||
.background(Color(UIColor.systemBackground))
|
||||
.cornerRadius(10)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.tint(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if support?.displayPurchaseHistoryLink == true {
|
||||
Button {
|
||||
viewModel.showAllPurchases = true
|
||||
} label: {
|
||||
CompatibilityLabeledContent(localization[.seeAllPurchases]) {
|
||||
Image(systemName: "chevron.forward")
|
||||
if support?.displayPurchaseHistoryLink == true {
|
||||
Button {
|
||||
viewModel.showAllPurchases = true
|
||||
} label: {
|
||||
CompatibilityLabeledContent(localization[.seeAllPurchases]) {
|
||||
Image(systemName: "chevron.forward")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 12, height: 12)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(Font.system(size: 12, weight: .bold))
|
||||
}
|
||||
.padding(16)
|
||||
.background(Color(UIColor.systemBackground))
|
||||
.cornerRadius(10)
|
||||
.tint(.primary)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
}
|
||||
}
|
||||
.applyIf(self.viewModel.screen.type == .management, apply: {
|
||||
$0.navigationTitle(self.viewModel.screen.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
@available(macOS, unavailable)
|
||||
@available(tvOS, unavailable)
|
||||
@@ -190,10 +216,21 @@ struct ManageSubscriptionsView_Previews: PreviewProvider {
|
||||
|
||||
// swiftlint:disable force_unwrapping
|
||||
static var previews: some View {
|
||||
let purchases = [
|
||||
CustomerCenterConfigData.subscriptionInformationYearlyExpiring(store: .amazon),
|
||||
CustomerCenterConfigData.subscriptionInformationMonthlyRenewing,
|
||||
CustomerCenterConfigData.subscriptionInformationFree
|
||||
let purchases: [PurchaseInformation] = [
|
||||
.yearlyExpiring(productIdentifier: "p1"),
|
||||
.yearlyExpiring(productIdentifier: "p2", store: .amazon),
|
||||
.yearlyExpiring(
|
||||
productIdentifier: "p3",
|
||||
introductoryDiscount: MockStoreProductDiscount.mock(
|
||||
paymentMode: .payAsYouGo,
|
||||
discountType: .introductory)),
|
||||
.yearlyExpiring(
|
||||
productIdentifier: "p4",
|
||||
introductoryDiscount: MockStoreProductDiscount.mock(
|
||||
paymentMode: .payUpFront,
|
||||
discountType: .promotional)),
|
||||
.monthlyRenewing,
|
||||
.subscriptionInformationFree
|
||||
]
|
||||
|
||||
let warningOffMock = CustomerCenterConfigData.mock(
|
||||
@@ -233,7 +270,6 @@ struct ManageSubscriptionsView_Previews: PreviewProvider {
|
||||
.environment(\.localization, CustomerCenterConfigData.default.localization)
|
||||
.environment(\.appearance, CustomerCenterConfigData.default.appearance)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// Copyright RevenueCat Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://opensource.org/licenses/MIT
|
||||
//
|
||||
// PurchaseInformationCardView.swift
|
||||
//
|
||||
// Created by Facundo Menzella on 7/5/25.
|
||||
|
||||
import RevenueCat
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS)
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
@available(macOS, unavailable)
|
||||
@available(tvOS, unavailable)
|
||||
@available(watchOS, unavailable)
|
||||
struct PurchaseInformationCardView: View {
|
||||
|
||||
private let title: String
|
||||
private let subtitle: String
|
||||
private let storeTitle: String
|
||||
private let showChevron: Bool
|
||||
|
||||
init(
|
||||
purchaseInformation: PurchaseInformation,
|
||||
showChevron: Bool = true,
|
||||
localization: CustomerCenterConfigData.Localization
|
||||
) {
|
||||
if purchaseInformation.title?.isEmpty == true {
|
||||
self.title = purchaseInformation.productIdentifier
|
||||
} else {
|
||||
self.title = purchaseInformation.title ?? purchaseInformation.productIdentifier
|
||||
}
|
||||
|
||||
self.subtitle = purchaseInformation.billingInformation(localizations: localization)
|
||||
self.storeTitle = localization[purchaseInformation.store.localizationKey]
|
||||
self.showChevron = showChevron
|
||||
}
|
||||
|
||||
init(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
storeTitle: String,
|
||||
showChevron: Bool = true
|
||||
) {
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.storeTitle = storeTitle
|
||||
self.showChevron = showChevron
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
CompatibilityLabeledContent {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
.padding(.bottom, 8)
|
||||
.frame(alignment: .leading)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.bottom, 4)
|
||||
.frame(alignment: .leading)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
Text(storeTitle)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(alignment: .leading)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
} content: {
|
||||
if showChevron {
|
||||
Image(systemName: "chevron.forward")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 12, height: 12)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(Font.system(size: 12, weight: .bold))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@available(iOS 15.0, *)
|
||||
@available(macOS, unavailable)
|
||||
@available(tvOS, unavailable)
|
||||
@available(watchOS, unavailable)
|
||||
struct PurchaseInformationCardView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
ScrollView {
|
||||
PurchaseInformationCardView(
|
||||
title: "Product name",
|
||||
subtitle: "Renews 24 May for $19.99",
|
||||
storeTitle: Store.appStore.localizationKey.rawValue
|
||||
)
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 2)
|
||||
.padding([.leading, .trailing])
|
||||
|
||||
PurchaseInformationCardView(
|
||||
title: "Product name",
|
||||
subtitle: "Renews 24 May for $19.99",
|
||||
storeTitle: Store.playStore.localizationKey.rawValue
|
||||
)
|
||||
.padding()
|
||||
.background(Color.white)
|
||||
.cornerRadius(10)
|
||||
.shadow(radius: 2)
|
||||
.padding([.leading, .trailing])
|
||||
}
|
||||
.environment(\.localization, CustomerCenterConfigData.default.localization)
|
||||
.environment(\.appearance, CustomerCenterConfigData.default.appearance)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -299,7 +299,7 @@ private struct PreviewContainer: View {
|
||||
|
||||
var body: some View {
|
||||
let purchaseInformationApple =
|
||||
CustomerCenterConfigData.subscriptionInformationMonthlyRenewing
|
||||
PurchaseInformation.monthlyRenewing
|
||||
let viewModelApple = CustomerCenterViewModel(
|
||||
purchaseInformation: purchaseInformationApple,
|
||||
configuration: CustomerCenterConfigData.default
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
//
|
||||
// Copyright RevenueCat Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://opensource.org/licenses/MIT
|
||||
//
|
||||
// SubscriptionDetailsView.swift
|
||||
//
|
||||
//
|
||||
// Created by Cody Kerns on 8/12/24.
|
||||
//
|
||||
|
||||
import RevenueCat
|
||||
import SwiftUI
|
||||
|
||||
#if os(iOS)
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
@available(macOS, unavailable)
|
||||
@available(tvOS, unavailable)
|
||||
@available(watchOS, unavailable)
|
||||
struct SubscriptionDetailsView: View {
|
||||
|
||||
let purchaseInformation: PurchaseInformation
|
||||
let refundRequestStatus: RefundRequestStatus?
|
||||
|
||||
@Environment(\.localization)
|
||||
private var localization: CustomerCenterConfigData.Localization
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
SubscriptionDetailsHeader(purchaseInformation: purchaseInformation, localization: localization)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Divider()
|
||||
.padding(.bottom)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16.0) {
|
||||
if let durationTitle = purchaseInformation.durationTitle {
|
||||
IconLabelView(
|
||||
iconName: "coloncurrencysign.arrow.circlepath",
|
||||
label: localization[.billingCycle],
|
||||
value: durationTitle
|
||||
)
|
||||
}
|
||||
|
||||
let priceValue: String? = {
|
||||
switch purchaseInformation.price {
|
||||
case .free:
|
||||
return localization[.free]
|
||||
case .paid(let localizedPrice):
|
||||
return localizedPrice
|
||||
case .unknown:
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
if let price = priceValue {
|
||||
IconLabelView(
|
||||
iconName: "coloncurrencysign",
|
||||
label: localization[.currentPrice],
|
||||
value: price
|
||||
)
|
||||
}
|
||||
|
||||
if let expirationOrRenewal = purchaseInformation.expirationOrRenewal {
|
||||
switch expirationOrRenewal.date {
|
||||
case .never:
|
||||
IconLabelView(
|
||||
iconName: "calendar",
|
||||
label: label(for: expirationOrRenewal),
|
||||
value: localization[.never]
|
||||
)
|
||||
case .date(let value):
|
||||
IconLabelView(
|
||||
iconName: "calendar",
|
||||
label: label(for: expirationOrRenewal),
|
||||
value: value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if let refundRequestStatus = refundRequestStatus,
|
||||
let refundStatusMessage = refundStatusMessage(for: refundRequestStatus) {
|
||||
IconLabelView(
|
||||
iconName: "arrowshape.turn.up.backward",
|
||||
label: localization[.refundStatus],
|
||||
value: refundStatusMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8.0)
|
||||
}
|
||||
|
||||
private func refundStatusMessage(for status: RefundRequestStatus) -> String? {
|
||||
switch status {
|
||||
case .error:
|
||||
return localization[.refundErrorGeneric]
|
||||
case .success:
|
||||
return localization[.refundGranted]
|
||||
case .userCancelled:
|
||||
return nil
|
||||
@unknown default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func label(for expirationOrRenewal: PurchaseInformation.ExpirationOrRenewal) -> String {
|
||||
switch expirationOrRenewal.label {
|
||||
case .nextBillingDate:
|
||||
return localization[.nextBillingDate]
|
||||
case .expires:
|
||||
return localization[.expires]
|
||||
case .expired:
|
||||
return localization[.expired]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
@available(macOS, unavailable)
|
||||
@available(tvOS, unavailable)
|
||||
@available(watchOS, unavailable)
|
||||
struct SubscriptionDetailsHeader: View {
|
||||
let purchaseInformation: PurchaseInformation
|
||||
let localization: CustomerCenterConfigData.Localization
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if let title = purchaseInformation.title {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
if let explanation = getSubscriptionExplanation(from: purchaseInformation, localization: localization) {
|
||||
Text(explanation)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
private func getSubscriptionExplanation(from purchaseInformation: PurchaseInformation,
|
||||
localization: CustomerCenterConfigData.Localization) -> String? {
|
||||
guard purchaseInformation.expirationOrRenewal != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch purchaseInformation.explanation {
|
||||
case .promotional:
|
||||
return localization[.youHavePromo]
|
||||
case .earliestRenewal:
|
||||
return localization[.subEarliestRenewal]
|
||||
case .earliestExpiration:
|
||||
return localization[.subEarliestExpiration]
|
||||
case .expired:
|
||||
return localization[.subExpired]
|
||||
case .lifetime:
|
||||
return localization[.youHaveLifetime]
|
||||
case .google:
|
||||
return localization[.googleSubscriptionManage]
|
||||
case .externalWeb:
|
||||
return localization[.webSubscriptionManage]
|
||||
case .otherStorePurchase:
|
||||
return localization[.pleaseContactSupportToManage]
|
||||
case .amazon:
|
||||
return localization[.amazonSubscriptionManage]
|
||||
case .rcWebBilling:
|
||||
return localization[.webSubscriptionManage]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
@available(macOS, unavailable)
|
||||
@available(tvOS, unavailable)
|
||||
@available(watchOS, unavailable)
|
||||
struct IconLabelView: View {
|
||||
let iconName: String
|
||||
let label: String
|
||||
let value: String
|
||||
|
||||
private let iconWidth = 22.0
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: iconName)
|
||||
.accessibilityHidden(true)
|
||||
.frame(width: iconWidth)
|
||||
VStack(alignment: .leading) {
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.textCase(.uppercase)
|
||||
Text(value)
|
||||
.font(.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
@available(macOS, unavailable)
|
||||
@available(tvOS, unavailable)
|
||||
@available(watchOS, unavailable)
|
||||
struct SubscriptionDetailsView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
ForEach(ColorScheme.allCases, id: \.self) { colorScheme in
|
||||
SubscriptionDetailsView(
|
||||
purchaseInformation: CustomerCenterConfigData.subscriptionInformationMonthlyRenewing,
|
||||
refundRequestStatus: .success
|
||||
)
|
||||
.preferredColorScheme(colorScheme)
|
||||
.previewDisplayName("Subscription Details - Monthly - \(colorScheme)")
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -144,6 +144,12 @@ public struct CustomerCenterConfigData: Equatable {
|
||||
case debugHeaderTitle = "Debug"
|
||||
case youMayHaveDuplicatedSubscriptionsTitle = "you_may_have_duplicated_subscriptions_title"
|
||||
case youMayHaveDuplicatedSubscriptionsSubtitle = "you_may_have_duplicated_subscriptions_subtitle"
|
||||
case pricePaid = "price_paid"
|
||||
case expiresOnDateWithoutChanges = "expires_on_date_without_changes"
|
||||
case renewsOnDateForPrice = "renews_on_date_for_price"
|
||||
case renewsOnDate = "renews_on_date"
|
||||
case priceAfterwards = "price_afterwards"
|
||||
case freeTrialUntilDate = "free_trial_until_date"
|
||||
|
||||
var defaultValue: String {
|
||||
switch self {
|
||||
@@ -334,6 +340,18 @@ public struct CustomerCenterConfigData: Equatable {
|
||||
case .youMayHaveDuplicatedSubscriptionsSubtitle:
|
||||
return "You might be subscribed both on the web and through the App Store." +
|
||||
"To avoid being charged twice, please cancel your iOS subscription in your device settings."
|
||||
case .pricePaid:
|
||||
return "Paid {{ price }}."
|
||||
case .expiresOnDateWithoutChanges:
|
||||
return "Expires on {{ date }} without further charges."
|
||||
case .renewsOnDateForPrice:
|
||||
return "Renews on {{ date }} for {{ price }}."
|
||||
case .renewsOnDate:
|
||||
return "Renews on {{ date }}."
|
||||
case .priceAfterwards:
|
||||
return "{{ price }} afterwards."
|
||||
case .freeTrialUntilDate:
|
||||
return "Free trial until {{ date }}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ final class CustomerCenterViewModelTests: TestCase {
|
||||
|
||||
await viewModel.loadScreen()
|
||||
|
||||
expect(viewModel.purchaseInformation) == viewModel.activePurchases.first
|
||||
expect(viewModel.purchaseInformation?.productIdentifier) == viewModel.activePurchases.first?.productIdentifier
|
||||
let purchaseInformation = try XCTUnwrap(viewModel.activePurchases.first)
|
||||
|
||||
expect(purchaseInformation.store) == .appStore
|
||||
@@ -117,7 +117,7 @@ final class CustomerCenterViewModelTests: TestCase {
|
||||
|
||||
await viewModel.loadScreen()
|
||||
|
||||
expect(viewModel.purchaseInformation) == viewModel.activePurchases.first
|
||||
expect(viewModel.purchaseInformation?.productIdentifier) == viewModel.activePurchases.first?.productIdentifier
|
||||
let purchaseInformation = try XCTUnwrap(viewModel.activePurchases.first)
|
||||
|
||||
expect(purchaseInformation.store) == .playStore
|
||||
@@ -203,7 +203,7 @@ final class CustomerCenterViewModelTests: TestCase {
|
||||
// Assert
|
||||
expect(viewModel.state) == .success
|
||||
|
||||
expect(viewModel.purchaseInformation) == viewModel.activePurchases.first
|
||||
expect(viewModel.purchaseInformation?.productIdentifier) == viewModel.activePurchases.first?.productIdentifier
|
||||
let purchaseInformation = try XCTUnwrap(viewModel.activePurchases.first)
|
||||
expect(purchaseInformation.title) == "title"
|
||||
expect(purchaseInformation.durationTitle) == "1 month"
|
||||
|
||||
@@ -443,7 +443,7 @@ final class ManageSubscriptionViewModelTests: TestCase {
|
||||
signedData: signedData)
|
||||
|
||||
let screen = PurchaseInformationFixtures.screenWithPromo(offerID: offerIdentifierInJSON)
|
||||
let purchaseInformation = CustomerCenterConfigData.subscriptionInformationYearlyExpiring()
|
||||
let purchaseInformation = PurchaseInformation.yearlyExpiring()
|
||||
let viewModel = ManageSubscriptionViewModel(screen: screen,
|
||||
showPurchaseHistory: false,
|
||||
actionWrapper: CustomerCenterActionWrapper(),
|
||||
@@ -514,14 +514,14 @@ private extension PurchaseInformation {
|
||||
durationTitle: "",
|
||||
explanation: .lifetime,
|
||||
price: .paid(""),
|
||||
expirationOrRenewal: PurchaseInformation.ExpirationOrRenewal(label: .expires, date: .date("")),
|
||||
expirationOrRenewal: ExpirationOrRenewal(label: .expires, date: .date("")),
|
||||
productIdentifier: "",
|
||||
store: .appStore,
|
||||
isLifetime: true,
|
||||
isTrial: false,
|
||||
isCancelled: false,
|
||||
latestPurchaseDate: nil,
|
||||
customerInfoRequestedDate: customerInfoRequestedDate,
|
||||
isCancelled: false,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
)
|
||||
}
|
||||
@@ -548,9 +548,9 @@ private extension PurchaseInformation {
|
||||
store: store,
|
||||
isLifetime: false,
|
||||
isTrial: isTrial,
|
||||
isCancelled: isCancelled,
|
||||
latestPurchaseDate: latestPurchaseDate,
|
||||
customerInfoRequestedDate: customerInfoRequestedDate,
|
||||
isCancelled: isCancelled,
|
||||
managePurchaseURL: managementURL
|
||||
)
|
||||
}
|
||||
|
||||
@@ -72,11 +72,11 @@ final class PurchaseInformationTests: TestCase {
|
||||
|
||||
let subscriptionInfo = try XCTUnwrap(PurchaseInformation(
|
||||
entitlement: entitlement,
|
||||
subscribedProduct: mockProduct.toStoreProduct(),
|
||||
storeProduct: mockProduct.toStoreProduct(),
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
))
|
||||
|
||||
expect(subscriptionInfo.title) == "Monthly Product"
|
||||
@@ -125,13 +125,14 @@ final class PurchaseInformationTests: TestCase {
|
||||
let subscriptionInfo = try XCTUnwrap(
|
||||
PurchaseInformation(
|
||||
entitlement: entitlement,
|
||||
subscribedProduct: mockProduct.toStoreProduct(),
|
||||
storeProduct: mockProduct.toStoreProduct(),
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
expect(subscriptionInfo.title) == "Monthly Product"
|
||||
expect(subscriptionInfo.durationTitle) == "1 month"
|
||||
expect(subscriptionInfo.explanation) == .lifetime
|
||||
@@ -178,13 +179,14 @@ final class PurchaseInformationTests: TestCase {
|
||||
let subscriptionInfo = try XCTUnwrap(
|
||||
PurchaseInformation(
|
||||
entitlement: entitlement,
|
||||
subscribedProduct: mockProduct.toStoreProduct(),
|
||||
storeProduct: mockProduct.toStoreProduct(),
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
expect(subscriptionInfo.title) == "Monthly Product"
|
||||
expect(subscriptionInfo.durationTitle) == "1 month"
|
||||
expect(subscriptionInfo.explanation) == .earliestExpiration
|
||||
@@ -231,13 +233,14 @@ final class PurchaseInformationTests: TestCase {
|
||||
let subscriptionInfo = try XCTUnwrap(
|
||||
PurchaseInformation(
|
||||
entitlement: entitlement,
|
||||
subscribedProduct: mockProduct.toStoreProduct(),
|
||||
storeProduct: mockProduct.toStoreProduct(),
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
expect(subscriptionInfo.title) == "Monthly Product"
|
||||
expect(subscriptionInfo.durationTitle) == "1 month"
|
||||
expect(subscriptionInfo.explanation) == .expired
|
||||
@@ -273,8 +276,8 @@ final class PurchaseInformationTests: TestCase {
|
||||
entitlement: entitlement,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
@@ -313,8 +316,8 @@ final class PurchaseInformationTests: TestCase {
|
||||
entitlement: entitlement,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
@@ -353,8 +356,8 @@ final class PurchaseInformationTests: TestCase {
|
||||
entitlement: entitlement,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
@@ -393,8 +396,8 @@ final class PurchaseInformationTests: TestCase {
|
||||
entitlement: entitlement,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
@@ -433,8 +436,8 @@ final class PurchaseInformationTests: TestCase {
|
||||
entitlement: entitlement,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
@@ -474,8 +477,8 @@ final class PurchaseInformationTests: TestCase {
|
||||
entitlement: entitlement,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
@@ -514,8 +517,8 @@ final class PurchaseInformationTests: TestCase {
|
||||
entitlement: entitlement,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
@@ -554,8 +557,8 @@ final class PurchaseInformationTests: TestCase {
|
||||
entitlement: entitlement,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
@@ -594,8 +597,8 @@ final class PurchaseInformationTests: TestCase {
|
||||
entitlement: entitlement,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
expect(subscriptionInfo.title).to(beNil())
|
||||
@@ -633,8 +636,8 @@ final class PurchaseInformationTests: TestCase {
|
||||
entitlement: entitlement,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
@@ -673,8 +676,8 @@ final class PurchaseInformationTests: TestCase {
|
||||
entitlement: entitlement,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
@@ -708,11 +711,11 @@ final class PurchaseInformationTests: TestCase {
|
||||
let subscriptionInfo = try XCTUnwrap(
|
||||
PurchaseInformation(
|
||||
entitlement: nil,
|
||||
subscribedProduct: nil,
|
||||
storeProduct: nil,
|
||||
transaction: mockTransaction,
|
||||
customerInfoRequestedDate: Date(),
|
||||
dateFormatter: Self.mockDateFormatter,
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!
|
||||
managePurchaseURL: URL(string: "https://www.revenuecat.com")!,
|
||||
dateFormatter: Self.mockDateFormatter
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in new issue
Block a user