Skip to content

Commit

Permalink
[Customer Center] Create CustomerCenterView (#3919)
Browse files Browse the repository at this point in the history
Base branch is `integration/customer_support_workflow` so we don't merge
into `main` yet

Borrows a lot from #3865 

Creates a new `CustomerCenterView` that can be used as a customer
support workflow starting point.

All details can be found in
https://linear.app/revenuecat/project/sdk-support-workflow-cf7f6a1d5340/overview

---------

Co-authored-by: Will Taylor <wtaylor151@gmail.com>
Co-authored-by: James Borthwick <109382862+jamesrb1@users.noreply.github.com>
  • Loading branch information
3 people committed Jun 28, 2024
1 parent 0ef63a7 commit 5fcbe86
Show file tree
Hide file tree
Showing 20 changed files with 1,965 additions and 2 deletions.
16 changes: 16 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@
3543914226F911F300E669DF /* MockSK1Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E524F6F5DC005BC22D /* MockSK1Product.swift */; };
3543914426F911F300E669DF /* MockCustomerInfoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 351B514826D44A2F00BD2BD7 /* MockCustomerInfoManager.swift */; };
3543914526F926D900E669DF /* SKProductSubscriptionDurationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DDF41E924F6F844005BC22D /* SKProductSubscriptionDurationExtensions.swift */; };
3544DA6D2C2C848E00704E9D /* CustomerCenterViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */; };
3544DA6F2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3544DA6A2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift */; };
354895D4267AE4B4001DC5B1 /* AttributionKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354895D3267AE4B4001DC5B1 /* AttributionKey.swift */; };
354895D6267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */; };
35549323269E298B005F9AE9 /* OfferingsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35549322269E298B005F9AE9 /* OfferingsFactory.swift */; };
Expand Down Expand Up @@ -1117,6 +1119,8 @@
352B7D7827BD919B002A47DD /* DangerousSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DangerousSettings.swift; sourceTree = "<group>"; };
3530C18822653E8F00D6DF52 /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; };
35316DA82BD14BFD00E4A970 /* MockDiagnosticsSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDiagnosticsSynchronizer.swift; sourceTree = "<group>"; };
3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomerCenterViewModelTests.swift; sourceTree = "<group>"; };
3544DA6A2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsViewModelTests.swift; sourceTree = "<group>"; };
354895D3267AE4B4001DC5B1 /* AttributionKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributionKey.swift; sourceTree = "<group>"; };
354895D5267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReservedSubscriberAttributes.swift; sourceTree = "<group>"; };
35549322269E298B005F9AE9 /* OfferingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfferingsFactory.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2525,6 +2529,15 @@
path = Purchasing;
sourceTree = "<group>";
};
3544DA6B2C2C848E00704E9D /* CustomerCenter */ = {
isa = PBXGroup;
children = (
3544DA692C2C848E00704E9D /* CustomerCenterViewModelTests.swift */,
3544DA6A2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift */,
);
path = CustomerCenter;
sourceTree = "<group>";
};
354895D0267AE32D001DC5B1 /* SubscriberAttributes */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3415,6 +3428,7 @@
887A62242C1D168B00E1A461 /* RevenueCatUITests */ = {
isa = PBXGroup;
children = (
3544DA6B2C2C848E00704E9D /* CustomerCenter */,
887A612D2C1D168B00E1A461 /* Data */,
887A61362C1D168B00E1A461 /* Helpers */,
887A61382C1D168B00E1A461 /* Purchasing */,
Expand Down Expand Up @@ -5028,8 +5042,10 @@
887A63352C1D177800E1A461 /* OSVersionEquivalent.swift in Sources */,
887A63362C1D177800E1A461 /* SnapshotTesting+Extensions.swift in Sources */,
887A63372C1D177800E1A461 /* TestCase.swift in Sources */,
3544DA6F2C2C848E00704E9D /* ManageSubscriptionsViewModelTests.swift in Sources */,
887A63382C1D177800E1A461 /* PurchaseHandlerTests.swift in Sources */,
887A63392C1D177800E1A461 /* OtherPaywallViewTests.swift in Sources */,
3544DA6D2C2C848E00704E9D /* CustomerCenterViewModelTests.swift in Sources */,
887A633A2C1D177800E1A461 /* PaywallViewDynamicTypeTests.swift in Sources */,
887A633B2C1D177800E1A461 /* PaywallViewLocalizationTests.swift in Sources */,
887A633C2C1D177800E1A461 /* Template1ViewTests.swift in Sources */,
Expand Down
59 changes: 57 additions & 2 deletions RevenueCatUI/CustomerCenter/Data/CustomerCenterConfigData.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,63 @@
//
// File.swift
// 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
//
// CustomerCenterConfigData.swift
//
//
// Created by Cesar de la Vega on 17/6/24.
// Created by Cesar de la Vega on 28/5/24.
//

import Foundation
import RevenueCat

struct CustomerCenterConfigData {

let id: String
let paths: [HelpPath]
let title: String

enum HelpPathType: String {
case missingPurchase = "MISSING_PURCHASE"
case refundRequest = "REFUND_REQUEST"
case changePlans = "CHANGE_PLANS"
case cancel = "CANCEL"
case unknown
}

enum HelpPathDetail {

case promotionalOffer(PromotionalOffer)
case feedbackSurvey(FeedbackSurvey)

}

struct HelpPath {

let id: String
let title: String
let type: HelpPathType
let detail: HelpPathDetail?

}

struct FeedbackSurvey {

let title: String
let options: [FeedbackSurveyOption]

}

struct FeedbackSurveyOption {

let id: String
let title: String

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// 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
//
// CustomerCenterConfigTestData.swift
//
//
// Created by Cesar de la Vega on 28/5/24.
//

import Foundation
import RevenueCat

enum CustomerCenterConfigTestData {

@available(iOS 14.0, *)
static let customerCenterData = CustomerCenterConfigData(
id: "customer_center_id",
paths: [
.init(
id: "1",
title: "Didn't receive purchase",
type: .missingPurchase,
detail: nil
),
.init(
id: "2",
title: "Request a refund",
type: .refundRequest,
detail: nil
),
.init(
id: "3",
title: "Change plans",
type: .changePlans,
detail: nil
),
.init(
id: "4",
title: "Cancel subscription",
type: .cancel,
detail: .feedbackSurvey(.init(
title: "Why are you cancelling?",
options: [
.init(
id: "1",
title: "Too expensive"
),
.init(
id: "2",
title: "Don't use the app"
),
.init(
id: "3",
title: "Bought by mistake"
)
]
))
)
],
title: "How can we help?"
)

static let subscriptionInformation: SubscriptionInformation = .init(
title: "Basic",
durationTitle: "Monthly",
price: "$4.99 / month",
nextRenewalString: "June 1st, 2024",
willRenew: true,
productIdentifier: "product_id",
active: true
)

}
41 changes: 41 additions & 0 deletions RevenueCatUI/CustomerCenter/Data/CustomerCenterError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// 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
//
// CustomerCenterError.swift
//
//
// Created by Cesar de la Vega on 29/5/24.
//

import Foundation

/// Error produced when displaying the customer center.
enum CustomerCenterError: Error {

/// Could not find information for an active subscription.
case couldNotFindSubscriptionInformation

}

extension CustomerCenterError: CustomNSError {

var errorUserInfo: [String: Any] {
return [
NSLocalizedDescriptionKey: self.description
]
}

private var description: String {
switch self {
case .couldNotFindSubscriptionInformation:
return "Could not find information for an active subscription."
}
}

}
50 changes: 50 additions & 0 deletions RevenueCatUI/CustomerCenter/Data/SubscriptionInformation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// 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
//
// SubscriptionInformation.swift
//
//
// Created by Cesar de la Vega on 28/5/24.
//

import Foundation

struct SubscriptionInformation {

let title: String
let durationTitle: String
let price: String
let nextRenewalString: String?
let productIdentifier: String

var renewalString: String {
return active ? (willRenew ? "Renews" : "Expires") : "Expired"
}

private let willRenew: Bool
private let active: Bool

init(title: String,
durationTitle: String,
price: String,
nextRenewalString: String?,
willRenew: Bool,
productIdentifier: String,
active: Bool
) {
self.title = title
self.durationTitle = durationTitle
self.price = price
self.nextRenewalString = nextRenewalString
self.productIdentifier = productIdentifier
self.willRenew = willRenew
self.active = active
}

}
50 changes: 50 additions & 0 deletions RevenueCatUI/CustomerCenter/ManageSubscriptionsButtonStyle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// 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
//
// CustomButtonStyle.swift
//
//
// Created by Cesar de la Vega on 28/5/24.
//

import Foundation
import SwiftUI

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
struct ManageSubscriptionsButtonStyle: ButtonStyle {

func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding()
.frame(width: 300)
.background(Color.accentColor)
.foregroundColor(.white)
.cornerRadius(10)
.scaleEffect(configuration.isPressed ? 0.95 : 1.0)
.opacity(configuration.isPressed ? 0.8 : 1.0)
.animation(.easeInOut(duration: 0.2), value: configuration.isPressed)
}

}

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
struct CustomButtonStylePreview_Previews: PreviewProvider {

static var previews: some View {
Button("Didn't receive purchase") {}
.buttonStyle(ManageSubscriptionsButtonStyle())
}

}
37 changes: 37 additions & 0 deletions RevenueCatUI/CustomerCenter/ManageSubscriptionsPurchaseType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// 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
//
// ManageSubscriptionsPurchaseType.swift
//
//
// Created by Cesar de la Vega on 12/6/24.
//

import Foundation
import RevenueCat

@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
@available(macOS, unavailable)
@available(tvOS, unavailable)
@available(watchOS, unavailable)
protocol ManageSubscriptionsPurchaseType: Sendable {

@Sendable
func customerInfo() async throws -> CustomerInfo

@Sendable
func products(_ productIdentifiers: [String]) async -> [StoreProduct]

@Sendable
func showManageSubscriptions() async throws

@Sendable
func beginRefundRequest(forProduct productID: String) async throws -> RefundRequestStatus

}
Loading

0 comments on commit 5fcbe86

Please sign in to comment.