Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh receipt for VPN + Public Receipt Viewer (uplift to 1.73.x) #26567

Merged
merged 1 commit into from
Nov 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
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
Loading