Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Ref #4879: VPN churn improvements - VerifyReceiptData fields with isB…
Browse files Browse the repository at this point in the history
…illingRetry (#7803)
  • Loading branch information
soner-yuksel authored Aug 2, 2023
1 parent 1127d32 commit d0e8bbe
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 22 deletions.
7 changes: 7 additions & 0 deletions App/Client.xcodeproj/Brave.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@
"identifier" : "PrivateCDNTests",
"name" : "PrivateCDNTests"
}
},
{
"target" : {
"containerPath" : "container:..",
"identifier" : "BraveVPNTests",
"name" : "BraveVPNTests"
}
}
],
"version" : 1
Expand Down
4 changes: 4 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ var package = Package(
name: "BraveSharedTests",
dependencies: ["BraveShared", "Preferences"]
),
.testTarget(
name: "BraveVPNTests",
dependencies: ["BraveVPN", "BraveShared", "GuardianConnect"]
),
.testTarget(
name: "BraveWalletTests",
dependencies: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,19 @@ import SafariServices
// MARK: - Callouts

extension BrowserViewController {

// Priority: P3A - Bottom Bar - VPN - Default Browser - Rewards - Cookie Notification - Link Receipt
/*
Check FullScreenCalloutType to make alterations to priority of pop-over variation

Priority:
- P3A
- VPN Update Billing
- Bottom Bar
- VPN Promotion
- Default Browser
- Rewards
- Cookie Notification
- VPN Link Receipt
*/
func presentFullScreenCallouts() {
for type in FullScreenCalloutType.allCases {
presentScreenCallout(for: type)
Expand All @@ -34,6 +45,8 @@ extension BrowserViewController {
switch type {
case .p3a:
presentP3AScreenCallout()
case .vpnUpdateBilling:
presentVPNUpdateBillingCallout(skipSafeGuards: skipSafeGuards)
case .bottomBar:
presentBottomBarCallout(skipSafeGuards: skipSafeGuards)
case .defaultBrowser:
Expand Down Expand Up @@ -244,11 +257,24 @@ extension BrowserViewController {
present(popup, animated: false)
}

private func presentVPNChurnPromoCallout(for type: VPNChurnPromoType) {
private func presentVPNUpdateBillingCallout(skipSafeGuards: Bool = false) {
if !skipSafeGuards {
// TODO: Condition
return
}

presentVPNChurnPromoCallout(for: .updateBillingExpired) {
// TODO: Action
}
}

// MARK: Helper Methods for Presentation

private func presentVPNChurnPromoCallout(for type: VPNChurnPromoType, completion: @escaping () -> Void) {
var vpnChurnPromoView = VPNChurnPromoView(churnPromoType: type)

vpnChurnPromoView.renewAction = {
// TODO: Action
completion()
}

let popup = PopupViewController(rootView: vpnChurnPromoView, isDismissable: true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,18 +129,18 @@ class RetentionPreferencesDebugMenuViewController: TableViewController {
.boolRow(
title: "VPN Callout Shown",
detailText: "Flag determining if VPN callout is shown to user.",
toggleValue: Preferences.FullScreenCallout.vpnCalloutCompleted.value,
toggleValue: Preferences.FullScreenCallout.vpnPromotionCalloutCompleted.value,
valueChange: {
if $0 {
let status = $0
Preferences.FullScreenCallout.vpnCalloutCompleted.value = status
Preferences.FullScreenCallout.vpnPromotionCalloutCompleted.value = status
}
},
cellReuseId: "VPNCalloutCell"),
.boolRow(
title: "Rewards Callout Shown",
detailText: "Flag determining if Rewards callout is shown to user.",
toggleValue: Preferences.FullScreenCallout.vpnCalloutCompleted.value,
toggleValue: Preferences.FullScreenCallout.vpnPromotionCalloutCompleted.value,
valueChange: {
if $0 {
let status = $0
Expand Down
103 changes: 100 additions & 3 deletions Sources/BraveVPN/BraveVPN.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ import os.log

/// A static class to handle all things related to the Brave VPN service.
public class BraveVPN {

public struct ReceiptResponse {
enum Status {
case active, expired, retryPeriod
}

enum ExpirationIntent: Int {
case none, cancelled, billingError, priceIncreaseConsent, notAvailable, unknown
}

var status: Status
var expiryReason: ExpirationIntent = .none
var expiryDate: Date?

var isInTrialPeriod: Bool = false
var autoRenewEnabled: Bool = false
}

private static let housekeepingApi = GRDHousekeepingAPI()
private static let helper = GRDVPNHelper.sharedInstance()
private static let serverManager = GRDServerManager()
Expand Down Expand Up @@ -80,9 +98,8 @@ public class BraveVPN {
return
}

// We validate the current receipt at the start to know if the subscription has expirerd.
BraveVPN.validateReceipt() { expired in
if expired == true {
BraveVPN.validateReceiptData() { receiptResponse in
if receiptResponse?.status == .expired {
clearConfiguration()
logAndStoreError("Receipt expired")
return
Expand Down Expand Up @@ -157,6 +174,86 @@ public class BraveVPN {
receiptHasExpired?(false)
}
}

/// Connects to Guardian's server to validate locally stored receipt.
/// Returns ReceiptResponse whoich hold information about status of receipt expiration etc
public static func validateReceiptData(receiptResponse: ((ReceiptResponse?) -> Void)? = nil) {
guard let receipt = receipt,
let bundleId = Bundle.main.bundleIdentifier else {
receiptResponse?(nil)
return
}

if Preferences.VPN.skusCredential.value != nil {
// Receipt verification applies to Apple's IAP only,
// if we detect Brave's SKU token we should not look at Apple's receipt.
return
}

housekeepingApi.verifyReceiptData(receipt, bundleId: bundleId) { response, error in
if let error = error {
// Error while fetching receipt response, the variations of error can be listed
// No App Store receipt data present
// Failed to retrieve receipt data from server
// Failed to decode JSON response data
receiptResponse?(nil)
logAndStoreError("Call for receipt verification failed: \(error.localizedDescription)")
return
}

guard let response = response else {
receiptResponse?(nil)
logAndStoreError("Receipt verification response is empty")
return
}

let receiptResponseItem = GRDIAPReceiptResponse(withReceiptResponse: response)
let processedReceiptDetail = BraveVPN.processReceiptResponse(receiptResponseItem: receiptResponseItem)

switch processedReceiptDetail.status {
case .expired:
Preferences.VPN.expirationDate.value = Date(timeIntervalSince1970: 1)
logAndStoreError("VPN Subscription LineItems are empty subscription expired", printToConsole: false)
case .active, .retryPeriod:
if let expirationDate = processedReceiptDetail.expiryDate {
Preferences.VPN.expirationDate.value = expirationDate
}

Preferences.VPN.freeTrialUsed.value = !processedReceiptDetail.isInTrialPeriod

populateRegionDataIfNecessary()
GRDSubscriptionManager.setIsPayingUser(true)
}

receiptResponse?(processedReceiptDetail)
}
}

public static func processReceiptResponse(receiptResponseItem: GRDIAPReceiptResponse) -> ReceiptResponse {
guard let newestReceiptLineItem = receiptResponseItem.lineItems.sorted(by: { $0.expiresDate > $1.expiresDate }).first else {
return ReceiptResponse(status: .expired)
}

let lineItemMetaData = receiptResponseItem.lineItemsMetadata.first(
where: { Int($0.originalTransactionId) ?? 00 == newestReceiptLineItem.originalTransactionId })

guard let metadata = lineItemMetaData else {
return ReceiptResponse(status: .active)
}

let receiptStatus: ReceiptResponse.Status = lineItemMetaData?.isInBillingRetryPeriod == true ? .retryPeriod : .active
// Expiration Intent is unsigned
let expirationIntent = ReceiptResponse.ExpirationIntent(rawValue: Int(metadata.expirationIntent)) ?? .none
// 0 is for turned off renewal, 1 is subscription renewal
let autoRenewEnabled = metadata.autoRenewStatus == 1

return ReceiptResponse(
status: receiptStatus,
expiryReason: expirationIntent,
expiryDate: newestReceiptLineItem.expiresDate,
isInTrialPeriod: newestReceiptLineItem.isTrialPeriod,
autoRenewEnabled: autoRenewEnabled)
}

// MARK: - STATE

Expand Down
9 changes: 7 additions & 2 deletions Sources/Onboarding/OnboardingPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ extension Preferences {
key: "fullScreenCallout.full-screen-bottom-bar-callout-completed",
default: false)

/// Whether the vpn callout is shown.
public static let vpnCalloutCompleted = Option<Bool>(
/// Whether the vpn promotion callout is shown.
public static let vpnPromotionCalloutCompleted = Option<Bool>(
key: "fullScreenCallout.full-screen-vpn-callout-completed",
default: false)

Expand All @@ -68,6 +68,11 @@ extension Preferences {
public static let omniboxCalloutCompleted = Option<Bool>(
key: "fullScreenCallout.full-screen-omnibox-callout-completed",
default: false)

/// Whether the vpn promotion callout is shown.
public static let vpnUpdateBillingCalloutCompleted = Option<Bool>(
key: "fullScreenCallout.full-screen-vpn-billing-callout-completed",
default: false)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,52 @@ import Preferences
import Growth

public enum FullScreenCalloutType: CaseIterable {
case bottomBar, p3a, rewards, defaultBrowser, blockCookieConsentNotices, vpnPromotion, vpnLinkReceipt
/*
The order will effect the priority

Priority:
- P3A
- VPN Update Billing
- Bottom Bar
- VPN Promotion
- Default Browser
- Rewards
- Cookie Notification
- VPN Link Receipt
*/
case p3a, vpnUpdateBilling, bottomBar, vpnPromotion, defaultBrowser, rewards, blockCookieConsentNotices, vpnLinkReceipt

/// The number of days passed to show certain type of callout
var period: Int {
switch self {
case .bottomBar: return 0
case .p3a: return 0
case .rewards: return 8
case .vpnUpdateBilling: return 0
case .bottomBar: return 0
case .vpnPromotion: return 4
case .defaultBrowser: return 10
case .rewards: return 8
case .blockCookieConsentNotices: return 0
case .vpnPromotion: return 4
case .vpnLinkReceipt: return 0
}
}

/// The preference value stored for complete state
public var preferenceValue: Preferences.Option<Bool> {
switch self {
case .bottomBar:
return Preferences.FullScreenCallout.bottomBarCalloutCompleted
case .p3a:
return Preferences.Onboarding.p3aOnboardingShown
case .rewards:
return Preferences.FullScreenCallout.rewardsCalloutCompleted
case .vpnUpdateBilling:
return Preferences.FullScreenCallout.vpnUpdateBillingCalloutCompleted
case .bottomBar:
return Preferences.FullScreenCallout.bottomBarCalloutCompleted
case .vpnPromotion:
return Preferences.FullScreenCallout.vpnPromotionCalloutCompleted
case .defaultBrowser:
return Preferences.DefaultBrowserIntro.completed
case .rewards:
return Preferences.FullScreenCallout.rewardsCalloutCompleted
case .blockCookieConsentNotices:
return Preferences.FullScreenCallout.blockCookieConsentNoticesCalloutCompleted
case .vpnPromotion:
return Preferences.FullScreenCallout.vpnCalloutCompleted
case .vpnLinkReceipt:
return Preferences.Onboarding.vpnLinkReceiptShown
}
Expand Down
Loading

0 comments on commit d0e8bbe

Please sign in to comment.