From 2b28348f4bf579e09ed63f48907b7fe5d927bfd6 Mon Sep 17 00:00:00 2001 From: Tamer Bader Date: Mon, 18 Nov 2024 19:42:30 -0800 Subject: [PATCH] Final sample app updates --- .../project.pbxproj | 14 +++-- .../Components/AppButtons.swift | 14 ----- .../Extensions/String+Extensions.swift | 2 +- .../Screens/Home/HomeView.swift | 57 ++++++++----------- .../Screens/Home/HomeViewModel.swift | 17 ------ .../Screens/Permissions/PermissionRow.swift | 4 +- .../Screens/Permissions/PermissionsView.swift | 50 +++++++++------- .../Permissions/PermissionsViewModel.swift | 13 ++++- 8 files changed, 74 insertions(+), 97 deletions(-) diff --git a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample.xcodeproj/project.pbxproj b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample.xcodeproj/project.pbxproj index 49b3fd9..3272b36 100644 --- a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample.xcodeproj/project.pbxproj +++ b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample.xcodeproj/project.pbxproj @@ -433,14 +433,15 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "Donut Counter"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Square uses Bluetooth to connect and communicate with Square readers and compatible accessories.\n"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Square needs to know where transactions take place to reduce risk and minimize payment disputes.\n"; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "Some Square readers use the microphone to communicate payment card data to your device.\n"; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Square uses location to know where transactions take place to reduce risk and minimize payment disputes.\n"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Square’s magstripe reader uses the microphone to communicate payment card data to your device."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen.storyboard"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -471,14 +472,15 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = "Donut Counter"; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Square uses Bluetooth to connect and communicate with Square readers and compatible accessories.\n"; - INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Square needs to know where transactions take place to reduce risk and minimize payment disputes.\n"; - INFOPLIST_KEY_NSMicrophoneUsageDescription = "Some Square readers use the microphone to communicate payment card data to your device.\n"; + INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Square uses location to know where transactions take place to reduce risk and minimize payment disputes.\n"; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Square’s magstripe reader uses the microphone to communicate payment card data to your device."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen.storyboard"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UIUserInterfaceStyle = Light; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Components/AppButtons.swift b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Components/AppButtons.swift index ecd0873..5e32b4e 100644 --- a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Components/AppButtons.swift +++ b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Components/AppButtons.swift @@ -40,20 +40,6 @@ struct AuthorizationButtonStyle: ButtonStyle { } } -struct MockReaderButtonStyle: ButtonStyle { - var isPresented: Bool - - func makeBody(configuration: Configuration) -> some View { - configuration.label - .frame(maxWidth: .infinity) - .padding(16) - .background(isPresented ? Color.Button.MockReader.hideMockReaderBackground : Color.Button.MockReader.showMockReaderBackground) - .foregroundStyle(Color.Button.MockReader.foreground) - .opacity(configuration.isPressed ? 0.8 : 1.0) - .clipShape(.rect(cornerRadius: 6)) - } -} - struct DismissButton: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label diff --git a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Extensions/String+Extensions.swift b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Extensions/String+Extensions.swift index f7402c9..b0971ef 100644 --- a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Extensions/String+Extensions.swift +++ b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Extensions/String+Extensions.swift @@ -44,7 +44,7 @@ extension String { enum Microphone { static var microphonePermissionTitle: String = "Microphone" - static var microphonePermissionDescription: String = "Square’s R4 reader uses the microphone jack to communicate payment card data to your device. You should ask for this permission if you are using an R4 reader." + static var microphonePermissionDescription: String = "Square’s magstripe reader uses the microphone to communicate payment card data to your device. You should ask for this permission if you are using a magstripe reader." } enum AuthorizationButton { diff --git a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Home/HomeView.swift b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Home/HomeView.swift index f2cad71..db9922a 100644 --- a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Home/HomeView.swift +++ b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Home/HomeView.swift @@ -6,19 +6,31 @@ import MockReaderUI #endif struct HomeView: View { + + private enum Constants { + static let headerViewBottomPadding: CGFloat = 50 + static let donutImageWidth: CGFloat = 248 + static let donutImageBottomPadding: CGFloat = 50 + static let appNameTextBottomPadding: CGFloat = 32 + static let authorizationStatusTextTopPadding: CGFloat = 10 + static let iPadPadding: CGFloat = 50 + } + + @SwiftUI.Environment(\.horizontalSizeClass) private var horizontalSizeClass @State private var presentingPermissionsView: Bool = false - @State var isMockReaderPresented: Bool = false - @State var viewModel: HomeViewModel + @State private var isMockReaderPresented: Bool = false + @State private var viewModel: HomeViewModel - private let viewHolder: MobilePaymentsSDKViewHolder = MobilePaymentsSDKViewHolder() private var mobilePaymentsSDK: SDKManager { viewModel.mobilePaymentsSDK } private var isIPad: Bool { - UIDevice.current.userInterfaceIdiom == .pad + horizontalSizeClass == .regular } private var isMockReaderAvailable: Bool { viewModel.authorizationState == .authorized && mobilePaymentsSDK.settingsManager.sdkSettings.environment == .sandbox } + + private let viewHolder: MobilePaymentsSDKViewHolder = MobilePaymentsSDKViewHolder() init(viewModel: HomeViewModel) { self.viewModel = viewModel @@ -33,16 +45,13 @@ struct HomeView: View { VStack { headerView contentView + Spacer() if isMockReaderAvailable { mockReaderButton } - Spacer() - } - .alert(isPresented: $viewModel.showPaymentStatusAlert) { - paymentStatusAlert } } - .padding([.leading, .trailing], isIPad ? 50 : nil) + .padding([.leading, .trailing], isIPad ? Constants.iPadPadding : nil) .padding([.top, .bottom]) .background(Color.white) .onChange(of: viewModel.authorizationState) { oldValue, newValue in @@ -61,7 +70,7 @@ struct HomeView: View { Spacer() permissionsButton } - .padding(.bottom, 50) + .padding(.bottom, Constants.headerViewBottomPadding) } private var contentView: some View { @@ -69,13 +78,13 @@ struct HomeView: View { Image("donut") .resizable() .aspectRatio(1.0, contentMode: .fit) - .frame(width: 248) - .padding(.bottom, 54) + .frame(width: Constants.donutImageWidth) + .padding(.bottom, Constants.donutImageBottomPadding) Text(String.Home.appTitle) .font(.title) .fontWeight(.bold) .foregroundColor(.black) - .padding([.bottom], 32) + .padding([.bottom], Constants.appNameTextBottomPadding) buyDonutButton .buttonStyle(BuyButtonStyle()) .font(.body) @@ -86,7 +95,7 @@ struct HomeView: View { .multilineTextAlignment(.leading) .font(.subheadline) .foregroundStyle(Color.Text.warning) - .padding(.top, 10) + .padding(.top, Constants.authorizationStatusTextTopPadding) } } } @@ -192,30 +201,10 @@ struct HomeView: View { Text(String.Home.MockReaderButton.showMockReaderTitle) } } - .buttonStyle(MockReaderButtonStyle(isPresented: isMockReaderPresented)) .font(.body) .fontWeight(.semibold) } - // MARK: - Alerts - - private var paymentStatusAlert: Alert { - switch viewModel.lastPaymentStatus { - case .completed(let payment): - Alert( - title: Text(String.Home.PaymentStatusAlert.paymentCompletedTitle), - message: Text("\(payment.paymentDescription?.debugDescription ?? "")") - ) - case .failure(let error): - Alert( - title: Text(String.Home.PaymentStatusAlert.paymentFailedTitle), - message: Text("\(error.localizedDescription)") - ) - case .canceled, .none: - Alert(title: Text(String.Home.PaymentStatusAlert.paymentCanceledTitle)) - } - } - // MARK: - Mock Reader UI private func presentMockReader() { diff --git a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Home/HomeViewModel.swift b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Home/HomeViewModel.swift index 085b476..e2b46f1 100644 --- a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Home/HomeViewModel.swift +++ b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Home/HomeViewModel.swift @@ -5,15 +5,7 @@ import MockReaderUI #endif @Observable class HomeViewModel: PaymentManagerDelegate { - - enum PaymentStatus { - case completed(Payment) - case failure(Error) - case canceled - } - var showPaymentStatusAlert: Bool = false - var lastPaymentStatus: PaymentStatus? = nil var authorizationState: AuthorizationState let mobilePaymentsSDK: SDKManager @@ -51,9 +43,6 @@ import MockReaderUI // for the transaction from your backend or generating it locally, prior to calling the // `startPayment` method. Config.localSalesID = String(UUID().uuidString.prefix(8)) - - lastPaymentStatus = .completed(payment) - showPaymentStatusAlert = true } func paymentManager( @@ -81,9 +70,6 @@ import MockReaderUI // idempotency key since it has been used, and a new key will be generated when the payment is restarted. idempotencyKeyStorage.delete(id: Config.localSalesID) } - - lastPaymentStatus = .failure(error) - showPaymentStatusAlert = true } func paymentManager( @@ -96,9 +82,6 @@ import MockReaderUI // It is essential to delete the idempotency key associated with this sale, allowing // a new key to be generated if the transaction is retried using the same custom ID. idempotencyKeyStorage.delete(id: Config.localSalesID) - - lastPaymentStatus = .canceled - showPaymentStatusAlert = true } private func refreshAuthorizationState() { diff --git a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionRow.swift b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionRow.swift index b1bdb26..d0e9dd2 100644 --- a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionRow.swift +++ b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionRow.swift @@ -47,9 +47,7 @@ struct PermissionsRow: View { .foregroundColor(Color.Permissions.iconColor) .padding(.leading, 16) .onTapGesture { - if !isPermissionGranted { - tapAction?() - } + tapAction?() } } .padding(.bottom, 15) diff --git a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionsView.swift b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionsView.swift index 917d2bd..d3f1996 100644 --- a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionsView.swift +++ b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionsView.swift @@ -3,24 +3,37 @@ import SquareMobilePaymentsSDK //add struct PermissionsView: View { - @State var viewModel: PermissionsViewModel - @Binding var presentingPermissionsView: Bool + private enum Constants { + static let permissionsViewBottomPadding: CGFloat = 16 + static let iPadPadding: CGFloat = 50 + static let authorizationButtonHeight: CGFloat = 48 + static let authorizationStatusTextTopPadding: CGFloat = 10 + } + + @SwiftUI.Environment(\.horizontalSizeClass) private var horizontalSizeClass + @State private var viewModel: PermissionsViewModel + @Binding private var presentingPermissionsView: Bool private var mobilePaymentsSDK: SDKManager { viewModel.mobilePaymentsSDK } private var isIPad: Bool { - UIDevice.current.userInterfaceIdiom == .pad + horizontalSizeClass == .regular } private var isAuthorized: Bool { viewModel.authorizationState == .authorized } + init(viewModel: PermissionsViewModel, presentingPermissionsView: Binding) { + self.viewModel = viewModel + self._presentingPermissionsView = presentingPermissionsView + } + var body: some View { ZStack { VStack(alignment: .center) { headerView ScrollView { permissionsView - .padding(.bottom, 16) + .padding(.bottom, Constants.permissionsViewBottomPadding) authorizationButton authorizationStatus Spacer() @@ -32,7 +45,7 @@ struct PermissionsView: View { UIScrollView.appearance().bounces = true } } - .padding([.leading, .trailing], isIPad ? 50 : nil) + .padding([.leading, .trailing], isIPad ? Constants.iPadPadding : nil) .padding([.top, .bottom]) } .background(.white) @@ -41,23 +54,22 @@ struct PermissionsView: View { // MARK: - Subviews private var headerView: some View { - HStack { - Button { - presentingPermissionsView = false - } label: { - Image(systemName: "xmark") - .font(.body) - .fontWeight(.medium) + ZStack { + HStack { + Button { + presentingPermissionsView = false + } label: { + Image(systemName: "xmark") + .font(.body) + .fontWeight(.medium) + } + .buttonStyle(DismissButton()) + Spacer() } - .buttonStyle(DismissButton()) - Spacer() Text(String.Permissions.headerTitle) .font(.title2) .fontWeight(.bold) .foregroundStyle(.black) - .padding(.trailing, 45) - - Spacer() } } @@ -111,7 +123,7 @@ struct PermissionsView: View { } } ) - .frame(height: 48) + .frame(height: Constants.authorizationButtonHeight) .buttonStyle(AuthorizationButtonStyle(isAuthorized: isAuthorized)) .font(.headline) .disabled(viewModel.isLoading) @@ -124,7 +136,7 @@ struct PermissionsView: View { .multilineTextAlignment(.leading) .font(.subheadline) .foregroundStyle(authorizationStatusForegroundColor) - .padding(.top, 10) + .padding(.top, Constants.authorizationStatusTextTopPadding) } // MARK: - Private Properties diff --git a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionsViewModel.swift b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionsViewModel.swift index 53c088a..d4d9e87 100644 --- a/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionsViewModel.swift +++ b/Example/SwiftUIExample/MobilePaymentsSwiftUIExample/Screens/Permissions/PermissionsViewModel.swift @@ -51,8 +51,15 @@ import SwiftUI } func requestMicrophone() { - AVCaptureDevice.requestAccess(for: .audio) { [weak self] _ in - self?.refreshMicrophonePermission() + switch AVAudioApplication.shared.recordPermission { + case .undetermined: + AVCaptureDevice.requestAccess(for: .audio) { [weak self] _ in + self?.refreshMicrophonePermission() + } + case .denied: + self.openAppSettings() + default: + return } } @@ -83,7 +90,7 @@ import SwiftUI } private func refreshMicrophonePermission() { - switch AVAudioSession.sharedInstance().recordPermission { + switch AVAudioApplication.shared.recordPermission { case .granted: isMicrophonePermissionGranted = true default: