From 63b8878c6fd605a389bbf4401bf245617718701a Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 13 Mar 2024 15:31:23 -0400 Subject: [PATCH 1/3] Fix buttons not being tappable throughout the app. Fix screen dismissing due to alerts and sheets being added on non-unary views --- .../Paywall/AIChatPaywallView.swift | 47 ++++++----- .../Settings/AIChatAdvancedSettingsView.swift | 81 ++++++++++++------- .../Settings/AIChatDefaultModelView.swift | 6 +- .../Subscription/SDK/BraveSkusSDK.swift | 8 ++ 4 files changed, 88 insertions(+), 54 deletions(-) diff --git a/ios/brave-ios/Sources/AIChat/Components/Paywall/AIChatPaywallView.swift b/ios/brave-ios/Sources/AIChat/Components/Paywall/AIChatPaywallView.swift index af40dd89ac82..14fe6bf20766 100644 --- a/ios/brave-ios/Sources/AIChat/Components/Paywall/AIChatPaywallView.swift +++ b/ios/brave-ios/Sources/AIChat/Components/Paywall/AIChatPaywallView.swift @@ -177,38 +177,41 @@ struct AIChatPaywallView: View { .frame(height: 1.0) .foregroundColor(Color(braveSystemName: .primitivePrimary70)) - VStack { - Button( - action: { - Task { await purchaseSubscription() } - }, - label: { + Button( + action: { + Task { await purchaseSubscription() } + }, + label: { + HStack { if paymentStatus == .ongoing { ProgressView() .tint(Color.white) + .padding() } else { Text(Strings.AIChat.paywallPurchaseActionTitle) .font(.body.weight(.semibold)) .foregroundColor(Color(.white)) + .padding() } } - ) - .frame(maxWidth: .infinity) - .padding() - .background( - LinearGradient( - gradient: - Gradient(colors: [ - Color(UIColor(rgb: 0xFF5500)), - Color(UIColor(rgb: 0xFF006B)), - ]), - startPoint: .init(x: 0.0, y: 0.0), - endPoint: .init(x: 0.0, y: 1.0) + .frame(maxWidth: .infinity) + .contentShape(ContainerRelativeShape()) + .background( + LinearGradient( + gradient: + Gradient(colors: [ + Color(UIColor(rgb: 0xFF5500)), + Color(UIColor(rgb: 0xFF006B)), + ]), + startPoint: .init(x: 0.0, y: 0.0), + endPoint: .init(x: 0.0, y: 1.0) + ) ) - ) - .clipShape(RoundedRectangle(cornerRadius: 16.0, style: .continuous)) - .disabled(paymentStatus == .ongoing) - } + } + ) + .clipShape(RoundedRectangle(cornerRadius: 16.0, style: .continuous)) + .disabled(paymentStatus == .ongoing) + .buttonStyle(.plain) .padding([.horizontal], 16.0) } } diff --git a/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatAdvancedSettingsView.swift b/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatAdvancedSettingsView.swift index c9f2ee8b0985..e2645ded4d29 100644 --- a/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatAdvancedSettingsView.swift +++ b/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatAdvancedSettingsView.swift @@ -200,22 +200,26 @@ public struct AIChatAdvancedSettingsView: View { title: Strings.AIChat.advancedSettingsSubscriptionStatusTitle, detail: subscriptionStatusTitle ) + .listRowBackground(Color(.secondaryBraveGroupedBackground)) AIChatAdvancedSettingsLabelDetailView( title: Strings.AIChat.advancedSettingsSubscriptionExpiresTitle, detail: expirationDateTitle ) + .listRowBackground(Color(.secondaryBraveGroupedBackground)) } else { // Subscription information is loading AIChatAdvancedSettingsLabelDetailView( title: Strings.AIChat.advancedSettingsSubscriptionStatusTitle, detail: nil ) + .listRowBackground(Color(.secondaryBraveGroupedBackground)) AIChatAdvancedSettingsLabelDetailView( title: Strings.AIChat.advancedSettingsSubscriptionExpiresTitle, detail: nil ) + .listRowBackground(Color(.secondaryBraveGroupedBackground)) } // Check subscription is activated with in-app purchase @@ -229,8 +233,11 @@ public struct AIChatAdvancedSettingsView: View { title: Strings.AIChat.advancedSettingsLinkPurchaseActionTitle, subtitle: Strings.AIChat.advancedSettingsLinkPurchaseActionSubTitle ) + .contentShape(Rectangle()) } ) + .buttonStyle(.plain) + .listRowBackground(Color(.secondaryBraveGroupedBackground)) if viewModel.isDevReceiptLinkingAvailable { Button( @@ -241,8 +248,11 @@ public struct AIChatAdvancedSettingsView: View { LabelView( title: "[Staging] Link receipt" ) + .contentShape(Rectangle()) } ) + .buttonStyle(.plain) + .listRowBackground(Color(.secondaryBraveGroupedBackground)) Button( action: { @@ -252,8 +262,11 @@ public struct AIChatAdvancedSettingsView: View { LabelView( title: "[Dev] Link receipt" ) + .contentShape(Rectangle()) } ) + .buttonStyle(.plain) + .listRowBackground(Color(.secondaryBraveGroupedBackground)) } Button( @@ -269,8 +282,11 @@ public struct AIChatAdvancedSettingsView: View { }, label: { premiumActionView + .contentShape(Rectangle()) } ) + .buttonStyle(.plain) + .listRowBackground(Color(.secondaryBraveGroupedBackground)) } } else { Button( @@ -283,29 +299,31 @@ public struct AIChatAdvancedSettingsView: View { }, label: { premiumActionView + .contentShape(Rectangle()) } ) + .buttonStyle(.plain) + .listRowBackground(Color(.secondaryBraveGroupedBackground)) + .sheet(isPresented: $isPaywallPresented) { + AIChatPaywallView( + premiumUpgrageSuccessful: { _ in + Task { @MainActor in + await model.refreshPremiumStatusOrderCredentials() + await viewModel.fetchOrder() + } + }) + } + .alert(isPresented: $appStoreConnectionErrorPresented) { + Alert( + title: Text(Strings.AIChat.appStoreErrorTitle), + message: Text(Strings.AIChat.appStoreErrorSubTitle), + dismissButton: .default(Text(Strings.OKString)) + ) + } } } header: { Text(Strings.AIChat.advancedSettingsSubscriptionHeaderTitle.uppercased()) } - .sheet(isPresented: $isPaywallPresented) { - AIChatPaywallView( - premiumUpgrageSuccessful: { _ in - Task { @MainActor in - await model.refreshPremiumStatusOrderCredentials() - await viewModel.fetchOrder() - } - }) - } - .alert(isPresented: $appStoreConnectionErrorPresented) { - Alert( - title: Text(Strings.AIChat.appStoreErrorTitle), - message: Text(Strings.AIChat.appStoreErrorSubTitle), - dismissButton: .default(Text(Strings.OKString)) - ) - } - .listRowBackground(Color(.secondaryBraveGroupedBackground)) Section { Button( @@ -315,33 +333,36 @@ public struct AIChatAdvancedSettingsView: View { label: { Text(Strings.AIChat.resetLeoDataActionTitle) .foregroundColor(Color(.braveBlurpleTint)) + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) } ) - .frame(maxWidth: .infinity) .listRowBackground(Color(.secondaryBraveGroupedBackground)) .buttonStyle(.plain) - } - .alert(isPresented: $resetAndClearAlertErrorPresented) { - Alert( - title: Text(Strings.AIChat.resetLeoDataErrorTitle), - message: Text(Strings.AIChat.resetLeoDataErrorDescription), - primaryButton: .destructive(Text(Strings.AIChat.resetLeoDataAlertButtonTitle)) { - model.clearAndResetData() - }, - secondaryButton: .cancel() - ) + .alert(isPresented: $resetAndClearAlertErrorPresented) { + Alert( + title: Text(Strings.AIChat.resetLeoDataErrorTitle), + message: Text(Strings.AIChat.resetLeoDataErrorDescription), + primaryButton: .destructive(Text(Strings.AIChat.resetLeoDataAlertButtonTitle)) { + model.clearAndResetData() + }, + secondaryButton: .cancel() + ) + } } } - .listBackgroundColor(Color(UIColor.braveGroupedBackground)) + .listBackgroundColor(Color(.braveGroupedBackground)) .listStyle(.insetGrouped) } var premiumActionView: some View { HStack { LabelView(title: subscriptionMenuTitle) - Spacer() + .frame(maxWidth: .infinity, alignment: .leading) + Image(braveSystemName: "leo.launch") .foregroundStyle(Color(braveSystemName: .iconDefault)) + .frame(alignment: .trailing) } } } diff --git a/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatDefaultModelView.swift b/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatDefaultModelView.swift index 33e9da3f3900..215cda608a6b 100644 --- a/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatDefaultModelView.swift +++ b/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatDefaultModelView.swift @@ -75,16 +75,19 @@ struct AIChatDefaultModelView: View { } } } + .contentShape(Rectangle()) } ) + .buttonStyle(.plain) + .listRowBackground(Color(.secondaryBraveGroupedBackground)) } } header: { Text(Strings.AIChat.defaultModelChatSectionTitle.uppercased()) } - .listRowBackground(Color(.secondaryBraveGroupedBackground)) } .listBackgroundColor(Color(UIColor.braveGroupedBackground)) .listStyle(.insetGrouped) + .navigationTitle(Strings.AIChat.defaultModelViewTitle) .sheet(isPresented: $isPresentingPaywallPremium) { AIChatPaywallView( premiumUpgrageSuccessful: { _ in @@ -93,6 +96,5 @@ struct AIChatDefaultModelView: View { } }) } - .navigationTitle(Strings.AIChat.defaultModelViewTitle) } } diff --git a/ios/brave-ios/Sources/AIChat/Subscription/SDK/BraveSkusSDK.swift b/ios/brave-ios/Sources/AIChat/Subscription/SDK/BraveSkusSDK.swift index 03a5814f9b18..3eef52466b1a 100644 --- a/ios/brave-ios/Sources/AIChat/Subscription/SDK/BraveSkusSDK.swift +++ b/ios/brave-ios/Sources/AIChat/Subscription/SDK/BraveSkusSDK.swift @@ -248,6 +248,10 @@ public class BraveSkusSDK { for group: BraveStoreProductGroup ) async throws -> SkusOrder { func decode(_ response: String) throws -> SkusOrder { + if response == "{}" { + throw SkusError.decodingError + } + guard let data = response.data(using: .utf8) else { throw SkusError.decodingError } @@ -271,6 +275,10 @@ public class BraveSkusSDK { for group: BraveStoreProductGroup ) async throws -> SkusCredentialSummary { func decode(_ response: String) throws -> SkusCredentialSummary { + if response == "{}" { + throw SkusError.decodingError + } + guard let data = response.data(using: .utf8) else { throw SkusError.decodingError } From a610759000e0a7a2d23d3501b17e789244f6b226 Mon Sep 17 00:00:00 2001 From: Brandon T Date: Wed, 13 Mar 2024 19:02:53 -0400 Subject: [PATCH 2/3] Update feedback logic to use the new backend changes --- .../AIChat/Components/AIChatView.swift | 29 ++++++++++++------- .../Feedback/AIChatFeedbackView.swift | 20 +++++++++++-- .../Preferences/AIChatPreferences.swift | 4 +++ 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift b/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift index 08c0d036d5d9..373ac78ab4d6 100644 --- a/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift +++ b/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift @@ -23,7 +23,7 @@ public struct AIChatView: View { private var lastMessageId @State - private var customFeedbackIndex: Int? + private var customFeedbackInfo: AIChatFeedbackModelToast? @State private var isPremiumPaywallPresented = false @@ -37,6 +37,9 @@ public struct AIChatView: View { @ObservedObject private var hasSeenIntro = Preferences.AIChat.hasSeenIntro + @ObservedObject + private var shouldShowFeedbackPremiumAd = Preferences.AIChat.showPremiumFeedbackAd + var openURL: ((URL) -> Void) public init(model: AIChatViewModel, openURL: @escaping (URL) -> Void) { @@ -129,7 +132,7 @@ public struct AIChatView: View { responseContextMenuItems(for: index, turn: turn) } - if let feedbackIndex = customFeedbackIndex, feedbackIndex == index { + if let feedbackInfo = customFeedbackInfo, feedbackInfo.turnId == index { feedbackView } } @@ -190,7 +193,7 @@ public struct AIChatView: View { .onChange(of: model.conversationHistory) { _ in scrollViewReader.scrollTo(lastMessageId, anchor: .bottom) } - .onChange(of: customFeedbackIndex) { _ in + .onChange(of: customFeedbackInfo) { _ in hideKeyboard() withAnimation { scrollViewReader.scrollTo(lastMessageId, anchor: .bottom) @@ -376,11 +379,11 @@ public struct AIChatView: View { onSelected: { Task { @MainActor in let ratingId = await model.rateConversation(isLiked: false, turnId: UInt(turnIndex)) - if ratingId != nil { + if let ratingId = ratingId { feedbackToast = .success( isLiked: false, onAddFeedback: { - customFeedbackIndex = turnIndex + customFeedbackInfo = AIChatFeedbackModelToast(turnId: turnIndex, ratingId: ratingId) } ) } else { @@ -429,8 +432,10 @@ public struct AIChatView: View { private var feedbackView: some View { AIChatFeedbackView( + premiumStatus: model.premiumStatus, + shouldShowPremiumAd: $shouldShowFeedbackPremiumAd.value, onSubmit: { category, feedback in - guard let feedbackIndex = customFeedbackIndex else { + guard let feedbackInfo = customFeedbackInfo else { feedbackToast = .error(message: Strings.AIChat.feedbackSubmittedErrorTitle) return } @@ -439,18 +444,18 @@ public struct AIChatView: View { let success = await model.submitFeedback( category: category, feedback: feedback, - ratingId: "\(feedbackIndex)" + ratingId: feedbackInfo.ratingId ) feedbackToast = success - ? .success(isLiked: true) : .error(message: Strings.AIChat.feedbackSubmittedErrorTitle) + ? .submitted : .error(message: Strings.AIChat.feedbackSubmittedErrorTitle) } - customFeedbackIndex = nil + customFeedbackInfo = nil }, onCancel: { - customFeedbackIndex = nil + customFeedbackInfo = nil feedbackToast = .none } ) @@ -459,7 +464,7 @@ public struct AIChatView: View { \.openURL, OpenURLAction { url in if url.host == "dismiss" { - //TODO: Dismiss feedback learn-more prompt + shouldShowFeedbackPremiumAd.value = false } else { openURL(url) dismiss() @@ -523,6 +528,8 @@ struct AIChatView_Preview: PreviewProvider { .background(Color(braveSystemName: .containerBackground)) AIChatFeedbackView( + premiumStatus: .inactive, + shouldShowPremiumAd: .constant(true), onSubmit: { print("Submitted Feedback: \($0) -- \($1)") }, diff --git a/ios/brave-ios/Sources/AIChat/Components/Feedback/AIChatFeedbackView.swift b/ios/brave-ios/Sources/AIChat/Components/Feedback/AIChatFeedbackView.swift index efe173981ec3..e68085e7208e 100644 --- a/ios/brave-ios/Sources/AIChat/Components/Feedback/AIChatFeedbackView.swift +++ b/ios/brave-ios/Sources/AIChat/Components/Feedback/AIChatFeedbackView.swift @@ -4,6 +4,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. import AVFoundation +import BraveCore import DesignSystem import Introspect import SpeechRecognition @@ -324,6 +325,11 @@ enum AIChatFeedbackOption: String, CaseIterable, Identifiable { } } +struct AIChatFeedbackModelToast: Equatable { + let turnId: Int + let ratingId: String +} + struct AIChatFeedbackView: View { @State private var category: AIChatFeedbackOption = .notHelpful @@ -331,6 +337,12 @@ struct AIChatFeedbackView: View { @State private var feedbackText: String = "" + @State + var premiumStatus: AiChat.PremiumStatus + + @Binding + var shouldShowPremiumAd: Bool + let onSubmit: (String, String) -> Void let onCancel: () -> Void @@ -353,8 +365,10 @@ struct AIChatFeedbackView: View { AIChatFeedbackInputView(text: $feedbackText) .padding([.horizontal, .bottom]) - AIChatFeedbackLeoPremiumAdView() - .padding(.horizontal) + if premiumStatus != .active && premiumStatus != .activeDisconnected && shouldShowPremiumAd { + AIChatFeedbackLeoPremiumAdView() + .padding(.horizontal) + } HStack { Button { @@ -385,6 +399,8 @@ struct AIChatFeedbackView: View { struct AIChatFeedbackView_Previews: PreviewProvider { static var previews: some View { AIChatFeedbackView( + premiumStatus: .inactive, + shouldShowPremiumAd: .constant(true), onSubmit: { print("Submitted Feedback: \($0) -- \($1)") }, diff --git a/ios/brave-ios/Sources/AIChat/Preferences/AIChatPreferences.swift b/ios/brave-ios/Sources/AIChat/Preferences/AIChatPreferences.swift index 50b7c30fc8c0..9ab937def865 100644 --- a/ios/brave-ios/Sources/AIChat/Preferences/AIChatPreferences.swift +++ b/ios/brave-ios/Sources/AIChat/Preferences/AIChatPreferences.swift @@ -28,6 +28,10 @@ extension Preferences { key: "aichat.autocompletesuggestions-enabled", default: true ) + public static let showPremiumFeedbackAd = Option( + key: "aichat.show-premium-feedback-ad", + default: true + ) } } From 43171f4976198cd1c9e0dd2ead8b366f694ebfe5 Mon Sep 17 00:00:00 2001 From: Brandon T Date: Thu, 14 Mar 2024 09:53:31 -0400 Subject: [PATCH 3/3] Addressed feedback. Reduced purchase restore timeout --- .../Sources/AIChat/Components/AIChatView.swift | 4 ++-- .../Feedback/AIChatFeedbackView.swift | 4 +--- .../Components/Paywall/AIChatPaywallView.swift | 17 +++++++---------- .../Settings/AIChatAdvancedSettingsView.swift | 1 - 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift b/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift index 373ac78ab4d6..cf78ae85b1d9 100644 --- a/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift +++ b/ios/brave-ios/Sources/AIChat/Components/AIChatView.swift @@ -433,7 +433,7 @@ public struct AIChatView: View { private var feedbackView: some View { AIChatFeedbackView( premiumStatus: model.premiumStatus, - shouldShowPremiumAd: $shouldShowFeedbackPremiumAd.value, + shouldShowPremiumAd: shouldShowFeedbackPremiumAd.value, onSubmit: { category, feedback in guard let feedbackInfo = customFeedbackInfo else { feedbackToast = .error(message: Strings.AIChat.feedbackSubmittedErrorTitle) @@ -529,7 +529,7 @@ struct AIChatView_Preview: PreviewProvider { AIChatFeedbackView( premiumStatus: .inactive, - shouldShowPremiumAd: .constant(true), + shouldShowPremiumAd: true, onSubmit: { print("Submitted Feedback: \($0) -- \($1)") }, diff --git a/ios/brave-ios/Sources/AIChat/Components/Feedback/AIChatFeedbackView.swift b/ios/brave-ios/Sources/AIChat/Components/Feedback/AIChatFeedbackView.swift index e68085e7208e..d83014ad0646 100644 --- a/ios/brave-ios/Sources/AIChat/Components/Feedback/AIChatFeedbackView.swift +++ b/ios/brave-ios/Sources/AIChat/Components/Feedback/AIChatFeedbackView.swift @@ -337,10 +337,8 @@ struct AIChatFeedbackView: View { @State private var feedbackText: String = "" - @State var premiumStatus: AiChat.PremiumStatus - @Binding var shouldShowPremiumAd: Bool let onSubmit: (String, String) -> Void @@ -400,7 +398,7 @@ struct AIChatFeedbackView_Previews: PreviewProvider { static var previews: some View { AIChatFeedbackView( premiumStatus: .inactive, - shouldShowPremiumAd: .constant(true), + shouldShowPremiumAd: true, onSubmit: { print("Submitted Feedback: \($0) -- \($1)") }, diff --git a/ios/brave-ios/Sources/AIChat/Components/Paywall/AIChatPaywallView.swift b/ios/brave-ios/Sources/AIChat/Components/Paywall/AIChatPaywallView.swift index 14fe6bf20766..7515a9880eee 100644 --- a/ios/brave-ios/Sources/AIChat/Components/Paywall/AIChatPaywallView.swift +++ b/ios/brave-ios/Sources/AIChat/Components/Paywall/AIChatPaywallView.swift @@ -229,13 +229,10 @@ struct AIChatPaywallView: View { } paymentStatus = .success - - Task.delayed(bySeconds: 2.0) { @MainActor in - shouldDismiss = true - } + shouldDismiss = true } catch { paymentStatus = .failure - isShowingPurchaseAlert.toggle() + isShowingPurchaseAlert = true } } @@ -246,11 +243,11 @@ struct AIChatPaywallView: View { if await storeSDK.restorePurchases() { iapRestoreTimer?.cancel() paymentStatus = .success - shouldDismiss.toggle() + shouldDismiss = true } else { iapRestoreTimer?.cancel() paymentStatus = .failure - isShowingPurchaseAlert.toggle() + isShowingPurchaseAlert = true } if iapRestoreTimer != nil { @@ -258,12 +255,12 @@ struct AIChatPaywallView: View { iapRestoreTimer = nil } - // Adding 1 minute time-out for restore - iapRestoreTimer = Task.delayed(bySeconds: 60.0) { @MainActor in + // Adding 30 seconds time-out for restore + iapRestoreTimer = Task.delayed(bySeconds: 30.0) { @MainActor in paymentStatus = .failure // Show Alert for failure of restore - isShowingPurchaseAlert.toggle() + isShowingPurchaseAlert = true } } } diff --git a/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatAdvancedSettingsView.swift b/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatAdvancedSettingsView.swift index e2645ded4d29..2d78499e111d 100644 --- a/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatAdvancedSettingsView.swift +++ b/ios/brave-ios/Sources/AIChat/Components/Settings/AIChatAdvancedSettingsView.swift @@ -362,7 +362,6 @@ public struct AIChatAdvancedSettingsView: View { Image(braveSystemName: "leo.launch") .foregroundStyle(Color(braveSystemName: .iconDefault)) - .frame(alignment: .trailing) } } }