diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift index d57c3597bdab..7d22adf7c5d4 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+Callout.swift @@ -25,33 +25,33 @@ extension BrowserViewController { } } - private func presentScreenCallout(for type: FullScreenCalloutType) { + private func presentScreenCallout(for type: FullScreenCalloutType, skipSafeGuards: Bool = false) { + // Check the type custom callout can be shown + guard shouldShowCallout(calloutType: type, skipSafeGuards: skipSafeGuards) else { + return + } + switch type { case .p3a: presentP3AScreenCallout() case .bottomBar: - presentBottomBarCallout() - case .vpn: - presentVPNAlertCallout() + presentBottomBarCallout(skipSafeGuards: skipSafeGuards) case .defaultBrowser: presentDefaultBrowserScreenCallout() case .rewards: - presentBraveRewardsScreenCallout() + presentBraveRewardsScreenCallout(skipSafeGuards: skipSafeGuards) case .blockCookieConsentNotices: - presentCookieNotificationBlockingCallout() - case .linkReceipt: - presentLinkReceiptCallout() + presentCookieNotificationBlockingCallout(skipSafeGuards: skipSafeGuards) + case .vpnPromotion: + presentVPNPromotionCallout(skipSafeGuards: skipSafeGuards) + case .vpnLinkReceipt: + presentVPNLinkReceiptCallout(skipSafeGuards: skipSafeGuards) } } // MARK: Conditional Callout Methods private func presentP3AScreenCallout() { - // Check the p3a callout can be shown - guard shouldShowCallout(calloutType: .p3a) else { - return - } - let onboardingP3ACalloutController = Welcome3PAViewController().then { $0.isModalInPresentation = true $0.modalPresentationStyle = .overFullScreen @@ -84,30 +84,22 @@ extension BrowserViewController { onboardingP3ACalloutController.setLayoutState(state: state) - if !isOnboardingOrFullScreenCalloutPresented { - braveCore.p3aUtils.isNoticeAcknowledged = true - present(onboardingP3ACalloutController, animated: false) - } + braveCore.p3aUtils.isNoticeAcknowledged = true + present(onboardingP3ACalloutController, animated: false) } - private func presentBottomBarCallout() { - guard traitCollection.userInterfaceIdiom == .phone else { return } - - // Check the bottom bar callout can be shown - guard shouldShowCallout(calloutType: .bottomBar) else { - return - } - - // Onboarding should be completed to show callouts - if Preferences.Onboarding.basicOnboardingCompleted.value != OnboardingState.completed.rawValue { - return + private func presentBottomBarCallout(skipSafeGuards: Bool = false) { + if !skipSafeGuards { + guard traitCollection.userInterfaceIdiom == .phone else { + return + } + + // Show if bottom bar is not enabled + if Preferences.General.isUsingBottomBar.value { + return + } } - // Show if bottom bar is not enabled - if Preferences.General.isUsingBottomBar.value { - return - } - var bottomBarView = OnboardingBottomBarView() bottomBarView.switchBottomBar = { [weak self] in guard let self else { return } @@ -128,46 +120,7 @@ extension BrowserViewController { present(popup, animated: false) } - private func presentVPNAlertCallout() { - // Check the vpn alert callout can be shown - guard shouldShowCallout(calloutType: .vpn) else { - return - } - - let onboardingNotCompleted = - Preferences.Onboarding.basicOnboardingCompleted.value != OnboardingState.completed.rawValue - - let showedPopup = Preferences.VPN.popupShowed - - if onboardingNotCompleted - || showedPopup.value - || !VPNProductInfo.isComplete { - FullScreenCalloutType.vpn.preferenceValue.value = false - return - } - - var vpnDetailsView = OnboardingVPNDetailsView() - vpnDetailsView.learnMore = { [weak self] in - guard let self = self else { return } - - self.dismiss(animated: false) { - self.presentCorrespondingVPNViewController() - } - } - - let popup = PopupViewController(rootView: vpnDetailsView, isDismissable: true) - - isOnboardingOrFullScreenCalloutPresented = true - showedPopup.value = true - present(popup, animated: false) - } - private func presentDefaultBrowserScreenCallout() { - // Check the defaultbrowser callout can be shown - guard shouldShowCallout(calloutType: .defaultBrowser) else { - return - } - let onboardingController = WelcomeViewController( state: WelcomeViewCalloutState.defaultBrowserCallout( info: WelcomeViewCalloutState.WelcomeViewDefaultBrowserDetails( @@ -195,61 +148,84 @@ extension BrowserViewController { ), p3aUtilities: braveCore.p3aUtils ) - if !isOnboardingOrFullScreenCalloutPresented { - present(onboardingController, animated: true) - } + present(onboardingController, animated: true) } - private func presentBraveRewardsScreenCallout() { - // Check the rewards callout can be shown - guard shouldShowCallout(calloutType: .rewards) else { - return - } - - if BraveRewards.isAvailable, !Preferences.Rewards.rewardsToggledOnce.value { - let controller = OnboardingRewardsAgreementViewController() - controller.onOnboardingStateChanged = { [weak self] controller, state in - self?.completeOnboarding(controller) - } - controller.onRewardsStatusChanged = { [weak self] status in - self?.rewards.isEnabled = status + private func presentBraveRewardsScreenCallout(skipSafeGuards: Bool = false) { + if !skipSafeGuards { + guard BraveRewards.isAvailable, !Preferences.Rewards.rewardsToggledOnce.value else { + return } - - present(controller, animated: true) - isOnboardingOrFullScreenCalloutPresented = true } + + let controller = OnboardingRewardsAgreementViewController() + controller.onOnboardingStateChanged = { [weak self] controller, state in + self?.completeOnboarding(controller) + } + controller.onRewardsStatusChanged = { [weak self] status in + self?.rewards.isEnabled = status + } + + isOnboardingOrFullScreenCalloutPresented = true + present(controller, animated: true) } - private func presentCookieNotificationBlockingCallout() { - // Check the blockCookieConsentNotices callout can be shown - guard shouldShowCallout(calloutType: .blockCookieConsentNotices) else { - return + private func presentCookieNotificationBlockingCallout(skipSafeGuards: Bool = false) { + if !skipSafeGuards { + // Show Cookie Block Callout if setting is enabled and on second launch + // After Basic onboarding is shown + guard !Preferences.General.isFirstLaunch.value, + !FilterListStorage.shared.isEnabled(for: FilterList.cookieConsentNoticesComponentID), + Preferences.FullScreenCallout.omniboxCalloutCompleted.value else { + return + } } - // Don't show this if we already enabled the setting - guard !FilterListStorage.shared.isEnabled(for: FilterList.cookieConsentNoticesComponentID) else { return } - // Don't show this if we are presenting another popup already - guard !isOnboardingOrFullScreenCalloutPresented else { return } - // We only show the popup on second launch - guard !Preferences.General.isFirstLaunch.value else { return } - // Ensure we successfully shown basic onboarding first - guard Preferences.FullScreenCallout.omniboxCalloutCompleted.value else { return } - let popover = PopoverController( contentController: CookieNotificationBlockingConsentViewController(yesCallback: { FilterListStorage.shared.enableFilterList(for: FilterList.cookieConsentNoticesComponentID, isEnabled: true) }), contentSizeBehavior: .preferredContentSize) popover.addsConvenientDismissalMargins = false + + isOnboardingOrFullScreenCalloutPresented = true popover.present(from: topToolbar.locationView.shieldsButton, on: self) } - private func presentLinkReceiptCallout(skipSafeGuards: Bool = false) { + private func presentVPNPromotionCallout(skipSafeGuards: Bool = false) { if !skipSafeGuards { - // Show this onboarding only if the VPN has been purchased - guard case .purchased = BraveVPN.vpnState else { return } + // Onboarding should be completed to show callouts + if Preferences.Onboarding.basicOnboardingCompleted.value != OnboardingState.completed.rawValue { + return + } - guard shouldShowCallout(calloutType: .linkReceipt) else { + if Preferences.VPN.popupShowed.value + || !VPNProductInfo.isComplete { + FullScreenCalloutType.vpnPromotion.preferenceValue.value = false + return + } + } + + var vpnDetailsView = OnboardingVPNDetailsView() + vpnDetailsView.learnMore = { [weak self] in + guard let self = self else { return } + + self.dismiss(animated: false) { + self.presentCorrespondingVPNViewController() + } + } + + let popup = PopupViewController(rootView: vpnDetailsView, isDismissable: true) + Preferences.VPN.popupShowed.value = true + + isOnboardingOrFullScreenCalloutPresented = true + present(popup, animated: false) + } + + private func presentVPNLinkReceiptCallout(skipSafeGuards: Bool = false) { + if !skipSafeGuards { + // Show this onboarding only if the VPN has been purchased + guard case .purchased = BraveVPN.vpnState else { return } @@ -258,16 +234,34 @@ extension BrowserViewController { } } - var linkReceiptView = OnboardingLinkReceiptView() + var linkReceiptView = VPNLinkReceiptView() linkReceiptView.linkReceiptAction = { self.openURLInNewTab(.brave.braveVPNLinkReceiptProd, isPrivate: PrivateBrowsingManager.shared.isPrivateBrowsing, isPrivileged: false) } let popup = PopupViewController(rootView: linkReceiptView, isDismissable: true) + + isOnboardingOrFullScreenCalloutPresented = true + present(popup, animated: false) + } + + private func presentVPNChurnPromoCallout(for type: VPNChurnPromoType) { + var vpnChurnPromoView = VPNChurnPromoView(churnPromoType: type) + + vpnChurnPromoView.renewAction = { + // TODO: Action + } + + let popup = PopupViewController(rootView: vpnChurnPromoView, isDismissable: true) + isOnboardingOrFullScreenCalloutPresented = true present(popup, animated: false) } - private func shouldShowCallout(calloutType: FullScreenCalloutType) -> Bool { + private func shouldShowCallout(calloutType: FullScreenCalloutType, skipSafeGuards: Bool) -> Bool { + if skipSafeGuards { + return true + } + if Preferences.DebugFlag.skipNTPCallouts == true || isOnboardingOrFullScreenCalloutPresented || topToolbar.inOverlayMode { return false } @@ -304,7 +298,7 @@ extension BrowserViewController { switch BraveVPN.vpnState { case .purchased: - presentLinkReceiptCallout(skipSafeGuards: true) + presentVPNLinkReceiptCallout(skipSafeGuards: true) case .expired, .notPurchased: if VPNProductInfo.isComplete { presentCorrespondingVPNViewController() diff --git a/Sources/BraveStrings/BraveStrings.swift b/Sources/BraveStrings/BraveStrings.swift index 7e26c7d1c776..a6e37964c36d 100644 --- a/Sources/BraveStrings/BraveStrings.swift +++ b/Sources/BraveStrings/BraveStrings.swift @@ -2576,6 +2576,11 @@ extension Strings { NSLocalizedString("vpn.checkboxBlockAds", tableName: "BraveShared", bundle: .module, value: "Blocks unwanted network connections", comment: "Text for a checkbox to present the user benefits for using Brave VPN") + + public static let checkboxBlockAdsAlternate = + NSLocalizedString("vpn.checkboxBlockAdsAlternate", tableName: "BraveShared", bundle: .module, + value: "Block ads & trackers across all apps ", + comment: "Text for a checkbox to present the user benefits for using Brave VPN") public static let checkboxGeoSelector = NSLocalizedString("vpn.checkboxGeoSelector", tableName: "BraveShared", bundle: .module, @@ -2587,6 +2592,11 @@ extension Strings { value: "Supports speeds of up to 100 Mbps", comment: "Text for a checkbox to present the user benefits for using Brave VPN") + public static let checkboxFastAlternate = + NSLocalizedString("vpn.checkboxFastAlternate", tableName: "BraveShared", bundle: .module, + value: "Fast and unlimited up to 100 Mbps", + comment: "Text for a checkbox to present the user benefits for using Brave VPN") + public static let checkboxNoSellout = NSLocalizedString("vpn.checkboxNoSellout", tableName: "BraveShared", bundle: .module, value: "We never share or sell your info", @@ -3036,8 +3046,98 @@ extension Strings { NSLocalizedString("vpn.vpnRegionSelectorButtonSubTitle", tableName: "BraveShared", bundle: .module, value: "Current Setting: %@", comment: "Button subtitle for VPN region selection in menu. %@ will be replaced with country name or automatic ex: Current Setting: Automatic") + + public static let autoRenewSoonExpirePopOverTitle = + NSLocalizedString("vpn.autoRenewSoonExpireTitle", tableName: "BraveShared", bundle: .module, + value: "Oh no! Your Brave VPN subscription is about to expire.", + comment: "Pop up title for VPN subscription is about expire") + + public static let autoRenewDiscountPopOverTitle = + NSLocalizedString("vpn.autoRenewDiscountPopOverTitle", tableName: "BraveShared", bundle: .module, + value: "Auto-renew your Brave VPN Subscription now and get 20% off for 3 months!", + comment: "Pop up title for renewing VPN subscription with discount") + + public static let autoRenewFreeMonthPopOverTitle = + NSLocalizedString("vpn.autoRenewFreeMonthPopOverTitle", tableName: "BraveShared", bundle: .module, + value: "Auto-renew your Brave VPN Subscription now and get 1 month free!", + comment: "Pop up title for renewing VPN subscription with month free") + + public static let updateBillingSoonExpirePopOverTitle = + NSLocalizedString("vpn.updateBillingSoonExpirePopOverTitle", tableName: "BraveShared", bundle: .module, + value: "There's a billing issue with your account, which means your Brave VPN subscription is about to expire.", + comment: "Pop up title for billing issue for subcription VPN about to expire") + + public static let updateBillingExpiredPopOverTitle = + NSLocalizedString("vpn.updateBillingExpiredPopOverTitle", tableName: "BraveShared", bundle: .module, + value: "Update your payment info to stay protected with Brave VPN.", + comment: "Pop up title for billing issue for subcription VPN already expired") + + public static let autoRenewSoonExpirePopOverDescription = + NSLocalizedString("vpn.autoRenewSoonExpirePopOverDescription", tableName: "BraveShared", bundle: .module, + value: "That means you'll lose Brave's extra protections for every app on your phone.", + comment: "Pop up description for VPN subscription is about expire") + + public static let updateBillingSoonExpirePopOverDescription = + NSLocalizedString("vpn.updateBillingSoonExpirePopOverDescription", tableName: "BraveShared", bundle: .module, + value: "Want to keep protecting every app on your phone? Just update your payment details.", + comment: "Pop up description for billing issue of subcription VPN about to expire") + public static let updateBillingExpiredPopOverDescription = + NSLocalizedString("vpn.updateBillingExpiredPopOverDescription", tableName: "BraveShared", bundle: .module, + value: "Don’t worry. We’ll keep VPN active for a few days while you are updating your payment info.", + comment: "Pop up description for billing issue of subcription VPN already expired") + + public static let autoReneSoonExpirePopOverSubDescription = + NSLocalizedString("vpn.autoReneSoonExpirePopOverSubDescription", tableName: "BraveShared", bundle: .module, + value: "Want to stay protected? Just renew before your subscription ends. As a thanks for renewing, we'll even take 20% off for the next 3 months.", + comment: "Pop up extra description for billing issue of subcription VPN about to expire") + + public static let autoRenewActionButtonTitle = + NSLocalizedString("vpn.autoRenewActionButtonTitle", tableName: "BraveShared", bundle: .module, + value: "Enable Auto-Renew", + comment: "Action button title that enables auto renew for subcription") + + public static let updatePaymentActionButtonTitle = + NSLocalizedString("vpn.updatePaymentActionButtonTitle", tableName: "BraveShared", bundle: .module, + value: "Update Payment", + comment: "Action button title that updates method payment") + + public static let subscribeVPNActionButtonTitle = + NSLocalizedString("vpn.subscribeVPNActionButtonTitle", tableName: "BraveShared", bundle: .module, + value: "Subscribe Now", + comment: "Action button title that subscribe action for VPN purchase") + + public static let subscribeVPNDiscountPopOverTitle = + NSLocalizedString("vpn.subscribeVPNDiscountPopOverTitle", tableName: "BraveShared", bundle: .module, + value: "Give Brave VPN another try and get 20% off for 3 months!", + comment: "Pop up title for subscribing VPN with discount") + + public static let subscribeVPNProtectionPopOverTitle = + NSLocalizedString("vpn.subscribeVPNProtectionPopOverTitle", tableName: "BraveShared", bundle: .module, + value: "Did you know that Brave VPN protects you outside of Brave Browser?", + comment: "Pop up title for subscribing VPN explaning VPN protects user outside the Brave") + + public static let subscribeVPNAllDevicesPopOverTitle = + NSLocalizedString("vpn.subscribeVPNAllDevicesPopOverTitle", tableName: "BraveShared", bundle: .module, + value: "Now, use Brave VPN on all your devices for the same price!", + comment: "Pop up title the subscription for VPN can be used for all platforms") + + public static let subscribeVPNProtectionPopOverDescription = + NSLocalizedString("vpn.subscribeVPNProtectionPopOverDescription", tableName: "BraveShared", bundle: .module, + value: "Brave VPN has always blocked trackers on every app, even outside the Brave browser. Now you can see who tried to track you, with the Brave Privacy Hub.", + comment: "Pop up description for subscribing VPN explaning VPN protects user outside the Brave") + + public static let subscribeVPNAllDevicesPopOverDescription = + NSLocalizedString("vpn.subscribeVPNAllDevicesPopOverDescription", tableName: "BraveShared", bundle: .module, + value: "That’s right. Your Brave VPN subscription is now good on up to 5 devices. So you can subscribe on iOS and use it on your Mac, Windows and Android devices for free.", + comment: "Pop up description the subscription for VPN can be used for all platforms") + + public static let subscribeVPNPopOverSubDescription = + NSLocalizedString("vpn.subscribeVPNPopOverSubDescription", tableName: "BraveShared", bundle: .module, + value: "Ready to safeguard every app on your phone? Come back to Brave VPN and get 20% off for the next 3 months.", + comment: "Pop up sub description the subscription for VPN can be used for all platforms") } + } extension Strings { diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/Contents.json b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/Contents.json new file mode 100644 index 000000000000..1e3ed154890e --- /dev/null +++ b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/Contents.json @@ -0,0 +1,54 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "auto_renew _discount_image@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "auto_renew _discount_image_mode_dark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "auto_renew _discount_image@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "auto_renew _discount_image_mode_dark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image@2x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image@2x.png new file mode 100644 index 000000000000..974010870fca Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image@2x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image@3x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image@3x.png new file mode 100644 index 000000000000..7acf9274bac4 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image@3x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image_mode_dark@2x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image_mode_dark@2x.png new file mode 100644 index 000000000000..5cbb1a872c48 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image_mode_dark@2x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image_mode_dark@3x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image_mode_dark@3x.png new file mode 100644 index 000000000000..2324e019dd94 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _discount_image.imageset/auto_renew _discount_image_mode_dark@3x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _free_image.imageset/Contents.json b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _free_image.imageset/Contents.json new file mode 100644 index 000000000000..982f38da97a9 --- /dev/null +++ b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _free_image.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "auto_renew _free_image@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "auto_renew _free_image@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _free_image.imageset/auto_renew _free_image@2x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _free_image.imageset/auto_renew _free_image@2x.png new file mode 100644 index 000000000000..abde174cbe13 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _free_image.imageset/auto_renew _free_image@2x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _free_image.imageset/auto_renew _free_image@3x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _free_image.imageset/auto_renew _free_image@3x.png new file mode 100644 index 000000000000..46c8ea1795d1 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _free_image.imageset/auto_renew _free_image@3x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/Contents.json b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/Contents.json new file mode 100644 index 000000000000..33e1c7a0a81b --- /dev/null +++ b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/Contents.json @@ -0,0 +1,54 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "auto_renew _soon_image@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "auto_renew _soon_image_mode_dark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "auto_renew _soon_image@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "auto_renew _soon_image_mode_dark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image@2x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image@2x.png new file mode 100644 index 000000000000..abfaef6f7162 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image@2x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image@3x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image@3x.png new file mode 100644 index 000000000000..5d5baa7e6f51 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image@3x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image_mode_dark@2x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image_mode_dark@2x.png new file mode 100644 index 000000000000..2b525532939c Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image_mode_dark@2x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image_mode_dark@3x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image_mode_dark@3x.png new file mode 100644 index 000000000000..a4653d46620f Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/auto_renew _soon_image.imageset/auto_renew _soon_image_mode_dark@3x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Contents.json b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Contents.json new file mode 100644 index 000000000000..a40f3a86124a --- /dev/null +++ b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Contents.json @@ -0,0 +1,54 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Illustration@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Illustration@2x 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Illustration@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Illustration@3x 1.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@2x 1.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@2x 1.png new file mode 100644 index 000000000000..f71bdda5e9ed Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@2x 1.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@2x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@2x.png new file mode 100644 index 000000000000..11996e9a3442 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@2x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@3x 1.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@3x 1.png new file mode 100644 index 000000000000..31933526dc76 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@3x 1.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@3x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@3x.png new file mode 100644 index 000000000000..92a8692bad2b Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_all-devices_image.imageset/Illustration@3x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_protection_image.imageset/Contents.json b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_protection_image.imageset/Contents.json new file mode 100644 index 000000000000..1ac6696e234e --- /dev/null +++ b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_protection_image.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "subscribe_voluntary_discount_image@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "subscribe_voluntary_discount_image@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_protection_image.imageset/subscribe_voluntary_discount_image@2x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_protection_image.imageset/subscribe_voluntary_discount_image@2x.png new file mode 100644 index 000000000000..7345d59a3b04 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_protection_image.imageset/subscribe_voluntary_discount_image@2x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_protection_image.imageset/subscribe_voluntary_discount_image@3x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_protection_image.imageset/subscribe_voluntary_discount_image@3x.png new file mode 100644 index 000000000000..3b28f7bcabd4 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/subscribe_protection_image.imageset/subscribe_voluntary_discount_image@3x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/Contents.json b/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/Contents.json new file mode 100644 index 000000000000..a4a8c4e52e85 --- /dev/null +++ b/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/Contents.json @@ -0,0 +1,54 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "update_billing_expired@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "update_billing_expired@2x 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "update_billing_expired@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "update_billing_expired@3x 1.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@2x 1.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@2x 1.png new file mode 100644 index 000000000000..3fd88962cf7c Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@2x 1.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@2x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@2x.png new file mode 100644 index 000000000000..3ebfb6aca382 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@2x.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@3x 1.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@3x 1.png new file mode 100644 index 000000000000..05f1f14e1600 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@3x 1.png differ diff --git a/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@3x.png b/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@3x.png new file mode 100644 index 000000000000..05b55a581678 Binary files /dev/null and b/Sources/Onboarding/Assets/Images.xcassets/VPN/update_billing_expired.imageset/update_billing_expired@3x.png differ diff --git a/Sources/Onboarding/OnboardingPreferences.swift b/Sources/Onboarding/OnboardingPreferences.swift index f3316e8caf01..c76d068d66fb 100644 --- a/Sources/Onboarding/OnboardingPreferences.swift +++ b/Sources/Onboarding/OnboardingPreferences.swift @@ -30,7 +30,7 @@ extension Preferences { default: false) /// Whether the link vpn receipt alert has been shown. - public static let linkReceiptShown = Option(key: "onboarding.link-receipt", default: false) + public static let vpnLinkReceiptShown = Option(key: "onboarding.link-receipt", default: false) /// Whether this is a new user who installed the application after onboarding retention updates public static let isNewRetentionUser = Option(key: "general.new-retention", default: nil) diff --git a/Sources/Onboarding/ProductNotifications/Resources/FullScreenCalloutManager.swift b/Sources/Onboarding/ProductNotifications/FullScreenCalloutManager.swift similarity index 88% rename from Sources/Onboarding/ProductNotifications/Resources/FullScreenCalloutManager.swift rename to Sources/Onboarding/ProductNotifications/FullScreenCalloutManager.swift index f4be5a7e5e01..28161bbb5b53 100644 --- a/Sources/Onboarding/ProductNotifications/Resources/FullScreenCalloutManager.swift +++ b/Sources/Onboarding/ProductNotifications/FullScreenCalloutManager.swift @@ -9,18 +9,18 @@ import Preferences import Growth public enum FullScreenCalloutType: CaseIterable { - case bottomBar, p3a, vpn, rewards, defaultBrowser, blockCookieConsentNotices, linkReceipt + case bottomBar, p3a, rewards, defaultBrowser, blockCookieConsentNotices, vpnPromotion, 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 .vpn: return 4 case .rewards: return 8 case .defaultBrowser: return 10 case .blockCookieConsentNotices: return 0 - case .linkReceipt: return 0 + case .vpnPromotion: return 4 + case .vpnLinkReceipt: return 0 } } @@ -31,16 +31,16 @@ public enum FullScreenCalloutType: CaseIterable { return Preferences.FullScreenCallout.bottomBarCalloutCompleted case .p3a: return Preferences.Onboarding.p3aOnboardingShown - case .vpn: - return Preferences.FullScreenCallout.vpnCalloutCompleted case .rewards: return Preferences.FullScreenCallout.rewardsCalloutCompleted case .defaultBrowser: return Preferences.DefaultBrowserIntro.completed case .blockCookieConsentNotices: return Preferences.FullScreenCallout.blockCookieConsentNoticesCalloutCompleted - case .linkReceipt: - return Preferences.Onboarding.linkReceiptShown + case .vpnPromotion: + return Preferences.FullScreenCallout.vpnCalloutCompleted + case .vpnLinkReceipt: + return Preferences.Onboarding.vpnLinkReceiptShown } } } diff --git a/Sources/Onboarding/VPNNotifications/VPNChurnPromoView.swift b/Sources/Onboarding/VPNNotifications/VPNChurnPromoView.swift new file mode 100644 index 000000000000..88ab17c3a2a2 --- /dev/null +++ b/Sources/Onboarding/VPNNotifications/VPNChurnPromoView.swift @@ -0,0 +1,266 @@ +// Copyright 2023 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 http://mozilla.org/MPL/2.0/. + +import SwiftUI +import Shared +import BraveShared +import DesignSystem +import BraveUI + +public enum VPNChurnPromoType { + case autoRenewSoonExpire + case autoRenewDiscount + case autoRenewFreeMonth + case updateBillingSoonExpire + case updateBillingExpired + case subscribeDiscount + case subscribeVPNProtection + case subscribeAllDevices + + var promoImage: String { + switch self { + case .autoRenewSoonExpire: + return "auto_renew _soon_image" + case .autoRenewDiscount: + return "auto_renew _discount_image" + case .autoRenewFreeMonth: + return "auto_renew _free_image" + case .updateBillingSoonExpire, .updateBillingExpired: + return "update_billing_expired" + case .subscribeDiscount: + return "auto_renew _soon_image" + case .subscribeVPNProtection: + return "subscribe_protection_image" + case .subscribeAllDevices: + return "subscribe_all-devices_image" + } + } + + var title: String { + switch self { + case .autoRenewSoonExpire: + return Strings.VPN.autoRenewSoonExpirePopOverTitle + case .autoRenewDiscount: + return Strings.VPN.autoRenewDiscountPopOverTitle + case .autoRenewFreeMonth: + return Strings.VPN.autoRenewFreeMonthPopOverTitle + case .updateBillingSoonExpire: + return Strings.VPN.updateBillingSoonExpirePopOverTitle + case .updateBillingExpired: + return Strings.VPN.updateBillingExpiredPopOverTitle + case .subscribeDiscount: + return Strings.VPN.subscribeVPNDiscountPopOverTitle + case .subscribeVPNProtection: + return Strings.VPN.subscribeVPNProtectionPopOverTitle + case .subscribeAllDevices: + return Strings.VPN.subscribeVPNAllDevicesPopOverTitle + } + } + + var description: String? { + switch self { + case .autoRenewSoonExpire: + return Strings.VPN.autoRenewSoonExpirePopOverDescription + case .updateBillingSoonExpire: + return Strings.VPN.updateBillingSoonExpirePopOverDescription + case .updateBillingExpired: + return Strings.VPN.updateBillingExpiredPopOverDescription + case .subscribeVPNProtection: + return Strings.VPN.subscribeVPNProtectionPopOverDescription + case .subscribeAllDevices: + return Strings.VPN.subscribeVPNAllDevicesPopOverDescription + default: + return nil + } + } + + var subDescription: String? { + switch self { + case .autoRenewSoonExpire: + return Strings.VPN.autoReneSoonExpirePopOverSubDescription + case .subscribeVPNProtection, .subscribeAllDevices: + return Strings.VPN.subscribeVPNPopOverSubDescription + default: + return nil + } + } + + var buttonTitle: String { + switch self { + case .autoRenewSoonExpire, .autoRenewDiscount, .autoRenewFreeMonth: + return Strings.VPN.autoRenewActionButtonTitle + case .updateBillingSoonExpire, .updateBillingExpired: + return Strings.VPN.updatePaymentActionButtonTitle + case .subscribeDiscount, .subscribeVPNProtection, .subscribeAllDevices: + return Strings.VPN.subscribeVPNActionButtonTitle + } + } +} + +public struct VPNChurnPromoView: View { + @Environment(\.presentationMode) @Binding private var presentationMode + @State private var height: CGFloat? + + public var renewAction: (() -> Void)? + + public var churnPromoType: VPNChurnPromoType + + private let descriptionItems = [Strings.VPN.checkboxBlockAdsAlternate, + Strings.VPN.popupCheckmarkSecureConnections, + Strings.VPN.checkboxFastAlternate, + Strings.VPN.popupCheckmark247Support] + + public init(churnPromoType: VPNChurnPromoType) { + self.churnPromoType = churnPromoType + } + + public var body: some View { + ScrollView { + VStack(spacing: 24) { + headerView + detailView + .padding(.bottom, 8) + footerView + } + .background { + GeometryReader { proxy in + Color.clear + .onAppear { height = proxy.size.height } + .onChange(of: proxy.size.height) { newValue in + height = newValue + } + } + } + .padding(.horizontal, 32) + } + .background(Color(.braveBackground)) + .frame(maxWidth: BraveUX.baseDimensionValue, maxHeight: height) + .overlay { + Button { + presentationMode.dismiss() + } label: { + Image(braveSystemName: "leo.close") + .renderingMode(.template) + .foregroundColor(Color(.bravePrimary)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing) + .padding([.top, .trailing], 10) + } + .padding(.vertical, 16) + .osAvailabilityModifiers { content in + #if compiler(>=5.8) + if #available(iOS 16.4, *) { + content + .scrollBounceBehavior(.basedOnSize) + } else { + content + .introspectScrollView { scrollView in + scrollView.alwaysBounceVertical = false + } + } + #else + content + .introspectScrollView { scrollView in + scrollView.alwaysBounceVertical = false + } + #endif + } + } + + private var headerView: some View { + VStack(spacing: 24) { + Image(churnPromoType.promoImage, bundle: .module) + + Text(churnPromoType.title) + .font(.title) + .multilineTextAlignment(.center) + } + } + + @ViewBuilder + private var detailView: some View { + switch churnPromoType { + case .autoRenewSoonExpire, .subscribeVPNProtection, .subscribeAllDevices: + let description = churnPromoType.description ?? "" + let subDescription = churnPromoType.subDescription ?? "" + + VStack(spacing: 24) { + Text(description) + .font(.title3) + .multilineTextAlignment(.center) + Text(subDescription) + .font(.callout) + .multilineTextAlignment(.center) + } + case .autoRenewDiscount, .autoRenewFreeMonth, .subscribeDiscount: + VStack(alignment: .leading, spacing: 8) { + ForEach(descriptionItems, id: \.self) { itemDescription in + HStack(spacing: 8) { + Image(sharedName: "vpn_checkmark_popup") + .renderingMode(.template) + .foregroundColor(Color(.red)) + .frame(alignment: .leading) + Text(itemDescription) + .multilineTextAlignment(.leading) + .foregroundColor(Color(.bravePrimary)) + .fixedSize(horizontal: false, vertical: true) + } + } + } + case .updateBillingExpired, .updateBillingSoonExpire: + let description = churnPromoType.description ?? "" + + Text(description) + .font(.title3) + .multilineTextAlignment(.center) + } + } + + private var footerView: some View { + VStack(spacing: 24) { + Button(action: { + renewAction?() + presentationMode.dismiss() + }) { + Text(churnPromoType.buttonTitle) + .padding(.vertical, 4) + .frame(maxWidth: .infinity) + } + .buttonStyle(BraveFilledButtonStyle(size: .large)) + + HStack(spacing: 8) { + Text(Strings.VPN.poweredBy) + .font(.footnote) + .foregroundColor(Color(.secondaryBraveLabel)) + .multilineTextAlignment(.center) + Image(sharedName: "vpn_brand") + .renderingMode(.template) + .foregroundColor(Color(.secondaryBraveLabel)) + } + .padding(.bottom, 16) + } + } +} + +#if DEBUG +struct VPNChurnPromoView_Previews: PreviewProvider { + static var previews: some View { + VPNChurnPromoView(churnPromoType: .autoRenewSoonExpire) + .previewLayout(.sizeThatFits) + + VPNChurnPromoView(churnPromoType: .autoRenewDiscount) + .previewLayout(.sizeThatFits) + + VPNChurnPromoView(churnPromoType: .autoRenewFreeMonth) + .previewLayout(.sizeThatFits) + + VPNChurnPromoView(churnPromoType: .updateBillingSoonExpire) + .previewLayout(.sizeThatFits) + + VPNChurnPromoView(churnPromoType: .updateBillingExpired) + .previewLayout(.sizeThatFits) + } +} +#endif diff --git a/Sources/Onboarding/Callouts/OnboardingLinkReceiptView.swift b/Sources/Onboarding/VPNNotifications/VPNLinkReceiptView.swift similarity index 93% rename from Sources/Onboarding/Callouts/OnboardingLinkReceiptView.swift rename to Sources/Onboarding/VPNNotifications/VPNLinkReceiptView.swift index 3c237d699daf..c653c2c4f343 100644 --- a/Sources/Onboarding/Callouts/OnboardingLinkReceiptView.swift +++ b/Sources/Onboarding/VPNNotifications/VPNLinkReceiptView.swift @@ -9,7 +9,7 @@ import BraveShared import DesignSystem import BraveUI -public struct OnboardingLinkReceiptView: View { +public struct VPNLinkReceiptView: View { @Environment(\.presentationMode) @Binding private var presentationMode public var linkReceiptAction: (() -> Void)? @@ -64,9 +64,9 @@ public struct OnboardingLinkReceiptView: View { } #if DEBUG -struct OnboardingLinkReceiptView_Previews: PreviewProvider { +struct VPNLinkReceiptView_Previews: PreviewProvider { static var previews: some View { - OnboardingLinkReceiptView() + VPNLinkReceiptView() .previewLayout(.sizeThatFits) } }