Skip to content

Commit

Permalink
Refresh receipt for VPN + Public Receipt Viewer (uplift to 1.73.x) (#…
Browse files Browse the repository at this point in the history
…26567)

Uplift of #26560 (squashed) to release
  • Loading branch information
brave-builds authored Nov 16, 2024
1 parent c74c0a2 commit 99ea028
Show file tree
Hide file tree
Showing 9 changed files with 464 additions and 2 deletions.
7 changes: 7 additions & 0 deletions ios/brave-ios/Sources/AIChat/AIChatStrings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,13 @@ extension Strings {
value: "Subscription",
comment: "The title for the header for subscription details"
)
public static let advancedSettingsViewReceiptTitle = NSLocalizedString(
"aichat.advancedSettingsViewReceiptTitle",
tableName: "BraveLeo",
bundle: .module,
value: "View AppStore Receipt",
comment: "The title for the button that allows the user to view the AppStore Receipt"
)
public static let appStoreErrorTitle = NSLocalizedString(
"aichat.appStoreErrorTitle",
tableName: "BraveLeo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import BraveCore
import BraveStore
import BraveUI
import DesignSystem
import Preferences
Expand Down Expand Up @@ -328,6 +329,16 @@ public struct AIChatAdvancedSettingsView: View {
)
}
}

// Check if there's an AppStore receipt and subscriptions have been loaded
if !viewModel.isSubscriptionStatusLoading && viewModel.inAppPurchaseSubscriptionState != nil
{
NavigationLink {
StoreKitReceiptSimpleView()
} label: {
LabelView(title: Strings.AIChat.advancedSettingsViewReceiptTitle)
}.listRowBackground(Color(.secondaryBraveGroupedBackground))
}
} header: {
Text(Strings.AIChat.advancedSettingsSubscriptionHeaderTitle.uppercased())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ class BraveSkusScriptHandler: TabContentScript {
switch method {
case .refreshOrder:
let order = try OrderMessage.from(message: message)
// Serialize to jsonObject???
return await skusManager.refreshOrder(for: order.orderId, domain: skusDomain)

case .fetchOrderCredentials:
Expand Down
11 changes: 10 additions & 1 deletion ios/brave-ios/Sources/BraveStore/Debug/StoreKitReceiptView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,16 @@ public struct StoreKitReceiptView: View {

NavigationLink(
destination: {
groupedProductsView(for: receipt.inAppPurchaseReceipts)
let purchaseDate = Date.now
let expirationDate = Date.distantFuture

groupedProductsView(
for: receipt.inAppPurchaseReceipts.sorted(by: {
$0.purchaseDate ?? purchaseDate > $1.purchaseDate ?? purchaseDate
|| $0.subscriptionExpirationDate ?? expirationDate > $1.subscriptionExpirationDate
?? expirationDate
})
)
},
label: {
Text("In-App Purchases")
Expand Down
219 changes: 219 additions & 0 deletions ios/brave-ios/Sources/BraveStore/Views/StoreKitReceiptSimpleView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
// Copyright 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

import BraveCore
import BraveStrings
import Introspect
import SwiftUI

private struct StoreKitReceiptSimpleLineView: View {
var title: String
var value: String

var body: some View {
HStack {
Text("\(title):")
.font(.headline)
.fixedSize(horizontal: false, vertical: true)
.frame(alignment: .leading)
.padding(.trailing, 16.0)

Text(value)
.font(.callout)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding()
}
}

public struct StoreKitReceiptSimpleView: View {
@State
private var loading: Bool = true

@State
private var base64EncodedReceipt: String?

@State
private var receipt: BraveStoreKitReceipt?

public init() {

}

public var body: some View {
VStack {
if let receipt = receipt {
List {
StoreKitReceiptSimpleLineView(
title: Strings.ReceiptViewer.applicationVersionTitle,
value: receipt.appVersion
)

StoreKitReceiptSimpleLineView(
title: Strings.ReceiptViewer.receiptDateTitle,
value: formatDate(receipt.receiptCreationDate)
)

productsView(for: groupProducts(receipt: receipt))
}
} else if loading {
ProgressView(Strings.ReceiptViewer.receiptViewerLoadingTitle)
.task {
if let receipt = try? AppStoreReceipt.receipt {
self.base64EncodedReceipt = receipt

if let data = Data(base64Encoded: receipt) {
self.receipt = BraveStoreKitReceipt(data: data)
loading = false
}
}
}
} else {
VStack {
Text(
base64EncodedReceipt == nil
? Strings.ReceiptViewer.noReceiptFoundTitle
: Strings.ReceiptViewer.receiptLoadingErrorTitle
)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity)
.padding(30.0)
}
}
}
.navigationTitle(Strings.ReceiptViewer.receiptViewerTitle)
.navigationViewStyle(.stack)
.introspectNavigationController { controller in
controller.navigationBar.topItem?.backButtonDisplayMode = .minimal
}
.toolbar {
if let base64EncodedReceipt {
ToolbarItem(placement: .navigationBarTrailing) {
ShareLink(item: base64EncodedReceipt) {
Label(Strings.ReceiptViewer.shareReceiptTitle, systemImage: "square.and.arrow.up")
}
.padding()
}
}
}
}

private func groupProducts(receipt: BraveStoreKitReceipt) -> [Purchase] {
let purchaseDate = Date.now
let expirationDate = Date.distantFuture
let purchases = receipt.inAppPurchaseReceipts.sorted(by: {
$0.purchaseDate ?? purchaseDate > $1.purchaseDate ?? purchaseDate
|| $0.subscriptionExpirationDate ?? expirationDate > $1.subscriptionExpirationDate
?? expirationDate
})

return Dictionary(grouping: purchases, by: { $0.productId }).compactMap {
(key, purchases) -> Purchase? in
guard let latestPurchase = purchases.first else {
return nil
}
return Purchase(productId: key, purchase: latestPurchase)
}
.sorted(by: { $0.productId < $1.productId })
}

@ViewBuilder
private func productsView(for products: [Purchase]) -> some View {
ForEach(products) { product in
NavigationLink(
destination: {
List {
purchaseView(for: product.purchase)
}
.navigationTitle(productName(from: product.productId))
.introspectNavigationController { controller in
controller.navigationBar.topItem?.backButtonDisplayMode = .minimal
}
},
label: {
Text(productName(from: product.productId))
.font(.headline)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)
}
)
.navigationBarTitleDisplayMode(.inline)
.buttonStyle(PlainButtonStyle())
.padding()
}
}

@ViewBuilder
private func purchaseView(for purchase: BraveStoreKitPurchase) -> some View {
VStack {
StoreKitReceiptSimpleLineView(
title: Strings.ReceiptViewer.receiptOrderIDTitle,
value: "\(purchase.webOrderLineItemId)"
)

StoreKitReceiptSimpleLineView(
title: Strings.ReceiptViewer.receiptTransactionIDTitle,
value: purchase.transactionId
)

StoreKitReceiptSimpleLineView(
title: Strings.ReceiptViewer.receiptOriginalPurchaseDateTitle,
value: formatDate(purchase.originalPurchaseDate)
)

StoreKitReceiptSimpleLineView(
title: Strings.ReceiptViewer.receiptPurchaseDate,
value: formatDate(purchase.purchaseDate)
)

StoreKitReceiptSimpleLineView(
title: Strings.ReceiptViewer.receiptExpirationDate,
value: formatDate(purchase.subscriptionExpirationDate)
)

if let cancellationDate = purchase.cancellationDate {
StoreKitReceiptSimpleLineView(
title: Strings.ReceiptViewer.receiptCancellationDate,
value: formatDate(cancellationDate)
)
}
}
}

private func productName(from bundleId: String) -> String {
switch bundleId {
case BraveStoreProduct.vpnMonthly.rawValue:
return Strings.ReceiptViewer.vpnMonthlySubscriptionName
case BraveStoreProduct.vpnYearly.rawValue:
return Strings.ReceiptViewer.vpnYearlySubscriptionName
case BraveStoreProduct.leoMonthly.rawValue:
return Strings.ReceiptViewer.leoMonthlySubscriptionName
case BraveStoreProduct.leoYearly.rawValue:
return Strings.ReceiptViewer.leoYearlySubscriptionName
default: return bundleId
}
}

private func formatDate(_ date: Date?) -> String {
guard let date = date else {
return Strings.ReceiptViewer.receiptInvalidDate
}

let formatter = DateFormatter()
formatter.dateStyle = .long
formatter.timeStyle = .long
return formatter.string(from: date)
}

private struct Purchase: Identifiable {
let productId: String
let purchase: BraveStoreKitPurchase

var id: String {
productId
}
}
}
Loading

0 comments on commit 99ea028

Please sign in to comment.