From a8fe8d4ba0acbfb46725d1d4793e0c6f03daefef Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Wed, 31 Jan 2024 11:57:45 +0000 Subject: [PATCH 1/4] feature: Native Pay Button --- .../project.pbxproj | 1 + .../AppConfiguration.swift | 1 + .../MobileBuyIntegration/AppDelegate.swift | 3 +- .../MobileBuyIntegration/LogsView.swift | 1 - .../SettingsViewController.swift | 28 ++++++ .../WebPixelEventsView.swift | 1 - Samples/SwiftUIExample/.swiftlint.yml | 1 - .../CheckoutBridge.swift | 9 ++ .../CheckoutWebView.swift | 70 ++++++++++++--- .../CheckoutWebViewController.swift | 56 +++++++++++- .../Configuration.swift | 10 +++ .../PayButtonView.swift | 86 +++++++++++++++++++ 12 files changed, 251 insertions(+), 16 deletions(-) delete mode 120000 Samples/SwiftUIExample/.swiftlint.yml create mode 100644 Sources/ShopifyCheckoutSheetKit/PayButtonView.swift diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj b/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj index f14192c5..ac43eb7b 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration.xcodeproj/project.pbxproj @@ -441,6 +441,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = A7XGC83MZE; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = MobileBuyIntegration/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/AppConfiguration.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/AppConfiguration.swift index 8d3cefb1..8a862d72 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/AppConfiguration.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/AppConfiguration.swift @@ -25,6 +25,7 @@ import ShopifyCheckoutSheetKit public struct AppConfiguration { public var useVaultedState: Bool = false + public var useNativeButton: Bool = false internal let webPixelsLogger = FileLogger("analytics.txt") } diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift index b0ee746a..be52534b 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift @@ -32,8 +32,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { /// Checkout color scheme setting $0.colorScheme = .automatic - /// Enable preloading - $0.preloading.enabled = true + /// Optional logger used for internal purposes $0.logger = FileLogger("log.txt") } diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/LogsView.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/LogsView.swift index b49a9f68..ca71e5b5 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/LogsView.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/LogsView.swift @@ -49,7 +49,6 @@ struct LogsView: View { Button(action: clearLogs) { Text("Clear logs") .foregroundColor(.red) - .background(.white) .font(.system(size: 12)) } Spacer() diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift index 879f9f9f..14e7d708 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift @@ -28,6 +28,7 @@ import ShopifyCheckoutSheetKit struct SettingsView: View { @State private var preloadingEnabled = ShopifyCheckoutSheetKit.configuration.preloading.enabled @State private var useVaultedState = appConfiguration.useVaultedState + @State private var useNativePayButton = ShopifyCheckoutSheetKit.configuration.payButton.enabled @State private var logs: [String?] = LogReader.shared.readLogs() ?? [] @State private var selectedColorScheme = ShopifyCheckoutSheetKit.configuration.colorScheme @State private var colorScheme: ColorScheme = .light @@ -44,6 +45,11 @@ struct SettingsView: View { .onChange(of: useVaultedState) { newValue in appConfiguration.useVaultedState = newValue } + Toggle("Native pay button", isOn: $useNativePayButton) + .onChange(of: useNativePayButton) { newValue in + appConfiguration.useNativeButton = newValue + ShopifyCheckoutSheetKit.configuration.payButton.enabled = newValue + } } Section(header: Text("Theme")) { @@ -158,6 +164,28 @@ extension Configuration.ColorScheme { } } + var payButtonBackgroundColor: UIColor { + switch self { + case .web: + return UIColor(red: 0.94, green: 0.94, blue: 0.91, alpha: 1.00) + default: + return .systemBackground + } + } + + var borderColor: UIColor { + switch self { + case .web: + return UIColor(red: 208/255, green: 208/255, blue: 205/255, alpha: 1.0) + case .light: + return UIColor(red: 222/255, green: 222/255, blue: 222/255, alpha: 1.0) + case .dark: + return UIColor(red: 68/255, green: 68/255, blue: 70/255, alpha: 1.0) + default: + return .systemGray5 + } + } + var backgroundColor: UIColor { switch self { case .web: diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/WebPixelEventsView.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/WebPixelEventsView.swift index 89941103..a5666a1f 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/WebPixelEventsView.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/WebPixelEventsView.swift @@ -48,7 +48,6 @@ struct WebPixelsEventsView: View { Button(action: clearLogs) { Text("Clear logs") .foregroundColor(.red) - .background(.white) .font(.system(size: 12)) } Spacer() diff --git a/Samples/SwiftUIExample/.swiftlint.yml b/Samples/SwiftUIExample/.swiftlint.yml deleted file mode 120000 index 7ec6f3a8..00000000 --- a/Samples/SwiftUIExample/.swiftlint.yml +++ /dev/null @@ -1 +0,0 @@ -Samples/MobileBuyIntegration/.swiftlint.yml \ No newline at end of file diff --git a/Sources/ShopifyCheckoutSheetKit/CheckoutBridge.swift b/Sources/ShopifyCheckoutSheetKit/CheckoutBridge.swift index 933a19b2..ecbbe008 100644 --- a/Sources/ShopifyCheckoutSheetKit/CheckoutBridge.swift +++ b/Sources/ShopifyCheckoutSheetKit/CheckoutBridge.swift @@ -52,6 +52,15 @@ enum CheckoutBridge { } let script = dispatchMessageTemplate(body: dispatchMessageBody) webView.evaluateJavaScript(script) + webView.evaluateJavaScript(""" + function showElement(selector) { + const el = document.querySelector(selector) + if (el) el.style.display = "block"; + } + showElement("#sticky-pay-button-container"); + showElement("#checkout-sdk-pay-button-container"); + showElement(".XlHGh"); + """) } static func decode(_ message: WKScriptMessage) throws -> WebEvent { diff --git a/Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift b/Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift index ecf60013..ee963433 100644 --- a/Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift +++ b/Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift @@ -103,7 +103,57 @@ class CheckoutWebView: WKWebView { configuration.userContentController .add(self, name: CheckoutBridge.messageHandler) + isBridgeAttached = true + + let script = """ + function hideElement(selector) { + const el = document.querySelector(selector) + if (el) el.style.display = "none"; + } + + function showElement(selector) { + const el = document.querySelector(selector) + if (el) el.style.display = "block"; + } + + function showStickyButtons() { + showElement("#sticky-pay-button-container"); + showElement("#checkout-sdk-pay-button-container"); + showElement(".XlHGh"); + } + + function hideStickyButtons() { + hideElement("#sticky-pay-button-container"); + hideElement("#checkout-sdk-pay-button-container"); + hideElement(".XlHGh"); + } + + document.addEventListener("DOMContentLoaded", () => { + const fullHeight = window.visualViewport.height; + showStickyButtons(); + + window.visualViewport.addEventListener('resize', () => { + if (window.visualViewport.height < fullHeight) { + hideStickyButtons(); + } + + if (window.visualViewport.height === fullHeight) { + showStickyButtons(); + } + }); + + document.addEventListener("focusout", () => { + showStickyButtons(); + }); + }); + """ + + configuration.userContentController.addUserScript(WKUserScript( + source: script, + injectionTime: WKUserScriptInjectionTime.atDocumentStart, + forMainFrameOnly: true + )) } deinit { @@ -158,7 +208,7 @@ extension CheckoutWebView: WKScriptMessageHandler { () } } catch { - viewDelegate?.checkoutViewDidFailWithError(error: .sdkError(underlying: error)) + viewDelegate?.checkoutViewDidFailWithError(error: .sdkError(underlying: error)) } } } @@ -182,15 +232,15 @@ extension CheckoutWebView: WKNavigationDelegate { decisionHandler(.allow) } - func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { - if let response = navigationResponse.response as? HTTPURLResponse { - decisionHandler(handleResponse(response)) - return - } - decisionHandler(.allow) - } + func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) { + if let response = navigationResponse.response as? HTTPURLResponse { + decisionHandler(handleResponse(response)) + return + } + decisionHandler(.allow) + } - func handleResponse(_ response: HTTPURLResponse) -> WKNavigationResponsePolicy { + func handleResponse(_ response: HTTPURLResponse) -> WKNavigationResponsePolicy { if isCheckout(url: response.url) && response.statusCode >= 400 { CheckoutWebView.cache = nil switch response.statusCode { @@ -257,7 +307,7 @@ extension CheckoutWebView: WKNavigationDelegate { } urlComponents.queryItems = urlComponents.queryItems?.filter { !($0.name == "open_externally") } return urlComponents.url ?? url - } + } private func isMailOrTelLink(_ url: URL) -> Bool { return ["mailto", "tel"].contains(url.scheme) diff --git a/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift b/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift index 204dbe9b..76f1f8bc 100644 --- a/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift +++ b/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift @@ -94,6 +94,10 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl ]) view.bringSubviewToFront(spinner) + if checkoutView.isLoading == false { + self.displayNativePayButton() + } + loadCheckout() } @@ -101,6 +105,50 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl checkoutView.checkoutDidPresent = true } + private func displayNativePayButton() { + guard ShopifyCheckoutSheetKit.configuration.payButton.enabled else { + if let payButtonView = self.view.viewWithTag(1337) { + payButtonView.removeFromSuperview() + } + return + } + let payButtonView = PayButtonView() + payButtonView.tag = 1337 + payButtonView.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(payButtonView) + + NSLayoutConstraint.activate([ + payButtonView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + payButtonView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + payButtonView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + self.checkoutView.evaluateJavaScript(""" + let style = document.createElement('style'); + document.head.appendChild(style); + style.appendChild(document.createTextNode('#pay-button-container { display: none !important; }')); + style.appendChild(document.createTextNode('#sticky-pay-button-container, .XlHGh, #checkout-sdk-pay-button-container { display: none !important; } footer {padding-bottom: 6em !important; padding-block-end: 9em !important}')); + + let shopPayButton = document.querySelector('button[aria-label="Pay now"]') + if (shopPayButton) shopPayButton.style.display = "none"; + """) + + payButtonView.buttonPressedAction = { + self.checkoutView.evaluateJavaScript("document.querySelector('#pay-button-container button')?.click()") + self.checkoutView.evaluateJavaScript("document.querySelector('button[aria-label=\"Pay now\"]')?.click()") + self.checkoutView.evaluateJavaScript("window.MobileCheckoutSdk.dispatchMessage('submitPayment');") + } + } + + public func removeNativePayButton() { + if ShopifyCheckoutSheetKit.configuration.payButton.enabled { + if let payButtonView = self.view.viewWithTag(1337) { + payButtonView.removeFromSuperview() + } + } + } + private func loadCheckout() { if checkoutView.url == nil { checkoutView.alpha = 0 @@ -129,7 +177,6 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl } extension CheckoutWebViewController: CheckoutWebViewDelegate { - func checkoutViewDidStartNavigation() { if initialNavigation && !checkoutView.checkoutDidLoad { spinner.startAnimating() @@ -139,14 +186,21 @@ extension CheckoutWebViewController: CheckoutWebViewDelegate { func checkoutViewDidFinishNavigation() { spinner.stopAnimating() initialNavigation = false + UIView.animate(withDuration: UINavigationController.hideShowBarDuration) { [weak checkoutView] in checkoutView?.alpha = 1 + if ShopifyCheckoutSheetKit.configuration.payButton.enabled { + self.displayNativePayButton() + } } } func checkoutViewDidCompleteCheckout() { ConfettiCannon.fire(in: view) CheckoutWebView.invalidate() + + self.removeNativePayButton() + delegate?.checkoutDidComplete() } diff --git a/Sources/ShopifyCheckoutSheetKit/Configuration.swift b/Sources/ShopifyCheckoutSheetKit/Configuration.swift index ab264e07..5991f89e 100644 --- a/Sources/ShopifyCheckoutSheetKit/Configuration.swift +++ b/Sources/ShopifyCheckoutSheetKit/Configuration.swift @@ -39,10 +39,14 @@ public struct Configuration { public var preloading = Configuration.Preloading() + public var payButton = Configuration.PayButton() + public var spinnerColor: UIColor = UIColor(red: 0.09, green: 0.45, blue: 0.69, alpha: 1.00) public var backgroundColor: UIColor = .systemBackground + public var borderColor: UIColor = .systemGray5 + public var logger: Logger = NoOpLogger() } @@ -67,6 +71,12 @@ extension Configuration { } } +extension Configuration { + public struct PayButton { + public var enabled: Bool = false + } +} + extension Configuration { public struct Preloading { public var enabled: Bool = true { diff --git a/Sources/ShopifyCheckoutSheetKit/PayButtonView.swift b/Sources/ShopifyCheckoutSheetKit/PayButtonView.swift new file mode 100644 index 00000000..cc990695 --- /dev/null +++ b/Sources/ShopifyCheckoutSheetKit/PayButtonView.swift @@ -0,0 +1,86 @@ +/* +MIT License + +Copyright 2023 - Present, Shopify Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +import UIKit + +class PayButtonView: UIView { + private var button: UIButton! + var buttonPressedAction: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + backgroundColor = ShopifyCheckoutSheetKit.configuration.backgroundColor + + let border = UIView() + border.backgroundColor = ShopifyCheckoutSheetKit.configuration.borderColor + border.autoresizingMask = [.flexibleWidth, .flexibleBottomMargin] + border.frame = CGRect(x: 0, y: 0, width: frame.size.width, height: 1) + addSubview(border) + + button = UIButton(type: .custom) + button.setTitle("Pay now", for: .normal) + button.setTitleColor(.white, for: .normal) + button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18) + button.backgroundColor = ShopifyCheckoutSheetKit.configuration.spinnerColor + button.layer.cornerRadius = 8 + button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 21, bottom: 0, right: 21) + button.layer.borderWidth = 1 + button.layer.borderColor = ShopifyCheckoutSheetKit.configuration.borderColor.cgColor + + button.translatesAutoresizingMaskIntoConstraints = false + addSubview(button) + + NSLayoutConstraint.activate([ + button.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 21), + button.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -21), + button.topAnchor.constraint(equalTo: topAnchor, constant: 18), + button.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -32), + button.heightAnchor.constraint(equalToConstant: 55) + ]) + + button.addTarget(self, action: #selector(buttonTouchUp), for: .touchUpInside) + button.addTarget(self, action: #selector(buttonTouchDown), for: .touchDown) + } + + @objc private func buttonTouchUp() { + buttonPressedAction?() + + UIView.animate(withDuration: 0.15, delay: 0.15, options: .curveEaseOut) { + self.button.backgroundColor = UIColor(red: 23/255, green: 115/255, blue: 176/255, alpha: 1.0) + } + } + + @objc private func buttonTouchDown() { + UIView.animate(withDuration: 0.15, delay: 0.0, options: .curveEaseOut) { + self.button.backgroundColor = UIColor(red: 16/255, green: 89/255, blue: 137/255, alpha: 1.0) + } + } +} From ed194eca77ab3e1b5865687a4c3e7eac3594587f Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Wed, 24 Jan 2024 15:19:28 +0000 Subject: [PATCH 2/4] [Experimental] Progress Bar --- .../AppConfiguration.swift | 3 + .../MobileBuyIntegration/AppDelegate.swift | 3 + .../SettingsViewController.swift | 5 ++ .../CheckoutWebViewController.swift | 84 +++++++++++++++---- .../Configuration.swift | 2 + .../IndeterminateProgressBarView.swift | 63 ++++++++++++++ 6 files changed, 145 insertions(+), 15 deletions(-) create mode 100644 Sources/ShopifyCheckoutSheetKit/IndeterminateProgressBarView.swift diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/AppConfiguration.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/AppConfiguration.swift index 8a862d72..1e17df23 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/AppConfiguration.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/AppConfiguration.swift @@ -24,8 +24,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO import ShopifyCheckoutSheetKit public struct AppConfiguration { + /// Prefill buyer information public var useVaultedState: Bool = false public var useNativeButton: Bool = false + + /// Logger to retain Web Pixel events internal let webPixelsLogger = FileLogger("analytics.txt") } diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift index be52534b..45a15d92 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift @@ -32,6 +32,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { /// Checkout color scheme setting $0.colorScheme = .automatic + /// Enable preloading + $0.preloading.enabled = true + /// Optional logger used for internal purposes $0.logger = FileLogger("log.txt") } diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift index 14e7d708..030ff1b9 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift @@ -32,6 +32,7 @@ struct SettingsView: View { @State private var logs: [String?] = LogReader.shared.readLogs() ?? [] @State private var selectedColorScheme = ShopifyCheckoutSheetKit.configuration.colorScheme @State private var colorScheme: ColorScheme = .light + @State private var useProgressBar = ShopifyCheckoutSheetKit.configuration.progressBarEnabled var body: some View { NavigationView { @@ -50,6 +51,10 @@ struct SettingsView: View { appConfiguration.useNativeButton = newValue ShopifyCheckoutSheetKit.configuration.payButton.enabled = newValue } + Toggle("Progress bar (experimental)", isOn: $useProgressBar) + .onChange(of: useProgressBar) { newValue in + ShopifyCheckoutSheetKit.configuration.progressBarEnabled = newValue + } } Section(header: Text("Theme")) { diff --git a/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift b/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift index 76f1f8bc..21ce9bc4 100644 --- a/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift +++ b/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift @@ -38,6 +38,12 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl return spinner }() + internal lazy var progress: IndeterminateProgressBarView = { + let progress = IndeterminateProgressBarView(frame: .zero) + progress.translatesAutoresizingMaskIntoConstraints = false + return progress + }() + internal var initialNavigation: Bool = true private let checkoutURL: URL @@ -72,6 +78,12 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl fatalError("init(coder:) has not been implemented") } + deinit { + if progressBarEnabled() { + checkoutView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress)) + } + } + // MARK: UIViewController Lifecycle override public func viewDidLoad() { @@ -87,12 +99,24 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl checkoutView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) - view.addSubview(spinner) - NSLayoutConstraint.activate([ - spinner.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), - spinner.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor) - ]) - view.bringSubviewToFront(spinner) + if progressBarEnabled() { + view.addSubview(progress) + NSLayoutConstraint.activate([ + progress.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + progress.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + progress.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + progress.heightAnchor.constraint(equalToConstant: 6) + ]) + view.bringSubviewToFront(progress) + checkoutView.addObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil) + } else { + view.addSubview(spinner) + NSLayoutConstraint.activate([ + spinner.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor), + spinner.centerYAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerYAnchor) + ]) + view.bringSubviewToFront(spinner) + } if checkoutView.isLoading == false { self.displayNativePayButton() @@ -101,6 +125,18 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl loadCheckout() } + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == #keyPath(WKWebView.estimatedProgress) { + let estimatedProgress = Float(checkoutView.estimatedProgress) + progress.setProgress(estimatedProgress, animated: true) + if estimatedProgress < 1.0 { + progress.startAnimating() + } else { + progress.stopAnimating() + } + } + } + func notifyPresented() { checkoutView.checkoutDidPresent = true } @@ -149,14 +185,28 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl } } + private func progressBarEnabled() -> Bool { + return ShopifyCheckoutSheetKit.configuration.progressBarEnabled + } + private func loadCheckout() { if checkoutView.url == nil { - checkoutView.alpha = 0 + if !progressBarEnabled() { + checkoutView.alpha = 0 + } initialNavigation = true checkoutView.load(checkout: checkoutURL) + + if progressBarEnabled() { + progress.startAnimating() + } } else if checkoutView.isLoading && initialNavigation { - checkoutView.alpha = 0 - spinner.startAnimating() + if progressBarEnabled() { + progress.startAnimating() + } else { + checkoutView.alpha = 0 + spinner.startAnimating() + } } } @@ -179,18 +229,22 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl extension CheckoutWebViewController: CheckoutWebViewDelegate { func checkoutViewDidStartNavigation() { if initialNavigation && !checkoutView.checkoutDidLoad { - spinner.startAnimating() + if !progressBarEnabled() { + spinner.startAnimating() + } } } func checkoutViewDidFinishNavigation() { - spinner.stopAnimating() initialNavigation = false - UIView.animate(withDuration: UINavigationController.hideShowBarDuration) { [weak checkoutView] in - checkoutView?.alpha = 1 - if ShopifyCheckoutSheetKit.configuration.payButton.enabled { - self.displayNativePayButton() + if !progressBarEnabled() { + spinner.stopAnimating() + UIView.animate(withDuration: UINavigationController.hideShowBarDuration) { [weak checkoutView] in + checkoutView?.alpha = 1 + if ShopifyCheckoutSheetKit.configuration.payButton.enabled { + self.displayNativePayButton() + } } } } diff --git a/Sources/ShopifyCheckoutSheetKit/Configuration.swift b/Sources/ShopifyCheckoutSheetKit/Configuration.swift index 5991f89e..0e4bedb3 100644 --- a/Sources/ShopifyCheckoutSheetKit/Configuration.swift +++ b/Sources/ShopifyCheckoutSheetKit/Configuration.swift @@ -48,6 +48,8 @@ public struct Configuration { public var borderColor: UIColor = .systemGray5 public var logger: Logger = NoOpLogger() + + public var progressBarEnabled: Bool = false } extension Configuration { diff --git a/Sources/ShopifyCheckoutSheetKit/IndeterminateProgressBarView.swift b/Sources/ShopifyCheckoutSheetKit/IndeterminateProgressBarView.swift new file mode 100644 index 00000000..d28ab196 --- /dev/null +++ b/Sources/ShopifyCheckoutSheetKit/IndeterminateProgressBarView.swift @@ -0,0 +1,63 @@ +import UIKit + +class IndeterminateProgressBarView: UIView { + private lazy var progressBar: UIProgressView = { + let progressBar = UIProgressView(progressViewStyle: .bar) + progressBar.setProgress(0.0, animated: false) + progressBar.translatesAutoresizingMaskIntoConstraints = false + return progressBar + }() + + private var progressAnimation: UIViewPropertyAnimator? + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(progressBar) + + NSLayoutConstraint.activate([ + progressBar.topAnchor.constraint(equalTo: topAnchor), + progressBar.heightAnchor.constraint(equalToConstant: 4), + ]) + + progressBar.tintColor = .systemGray5 + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + + if let superview = superview { + progressBar.leadingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.leadingAnchor).isActive = true + progressBar.trailingAnchor.constraint(equalTo: superview.safeAreaLayoutGuide.trailingAnchor).isActive = true + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func setProgress(_ progress: Float, animated: Bool = false) { + if (progress > progressBar.progress) { + progressBar.setProgress(progress, animated: animated) + } + } + + func startAnimating() { + print("start animating") + alpha = 1 + isHidden = false + } + + func stopAnimating() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + UIView.animate(withDuration: 0.2, animations: { + self.alpha = 0 + }) { _ in + self.isHidden = true + self.alpha = 1 + self.progressBar.setProgress(0.0, animated: false) + } + }) + + } +} From 9d18b82febc3cb5c1f61c0fe16435c7b6472bd23 Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Mon, 29 Jan 2024 18:51:52 +0000 Subject: [PATCH 3/4] Split experiments to its own section --- .../MobileBuyIntegration/SettingsViewController.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift index 030ff1b9..d667e152 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/SettingsViewController.swift @@ -46,12 +46,15 @@ struct SettingsView: View { .onChange(of: useVaultedState) { newValue in appConfiguration.useVaultedState = newValue } + } + + Section(header: Text("Experiments")) { Toggle("Native pay button", isOn: $useNativePayButton) .onChange(of: useNativePayButton) { newValue in appConfiguration.useNativeButton = newValue ShopifyCheckoutSheetKit.configuration.payButton.enabled = newValue } - Toggle("Progress bar (experimental)", isOn: $useProgressBar) + Toggle("Progress bar", isOn: $useProgressBar) .onChange(of: useProgressBar) { newValue in ShopifyCheckoutSheetKit.configuration.progressBarEnabled = newValue } From b97d93254d73b3787eb30b9f92ad7c387afdd45f Mon Sep 17 00:00:00 2001 From: Mark Murray Date: Mon, 29 Jan 2024 19:17:22 +0000 Subject: [PATCH 4/4] [Progress] Fix dark background color of webview --- .../MobileBuyIntegration/AppDelegate.swift | 4 +++- Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift | 7 +++++++ .../CheckoutWebViewController.swift | 8 ++++++-- .../IndeterminateProgressBarView.swift | 5 ++--- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift b/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift index 45a15d92..8abc9aed 100644 --- a/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift +++ b/Samples/MobileBuyIntegration/MobileBuyIntegration/AppDelegate.swift @@ -33,7 +33,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { $0.colorScheme = .automatic /// Enable preloading - $0.preloading.enabled = true + $0.preloading.enabled = false + + $0.progressBarEnabled = true /// Optional logger used for internal purposes $0.logger = FileLogger("log.txt") diff --git a/Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift b/Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift index ee963433..87bf786f 100644 --- a/Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift +++ b/Sources/ShopifyCheckoutSheetKit/CheckoutWebView.swift @@ -154,6 +154,13 @@ class CheckoutWebView: WKWebView { injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: true )) + + isOpaque = false + backgroundColor = ShopifyCheckoutSheetKit.configuration.backgroundColor + + if #available(iOS 15.0, *) { + underPageBackgroundColor = ShopifyCheckoutSheetKit.configuration.backgroundColor + } } deinit { diff --git a/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift b/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift index 21ce9bc4..4e20415c 100644 --- a/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift +++ b/Sources/ShopifyCheckoutSheetKit/CheckoutWebViewController.swift @@ -72,6 +72,8 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl navigationItem.rightBarButtonItem = closeBarButtonItem checkoutView.viewDelegate = self + + view.backgroundColor = ShopifyCheckoutSheetKit.configuration.backgroundColor } required init?(coder: NSCoder) { @@ -86,11 +88,13 @@ class CheckoutWebViewController: UIViewController, UIAdaptivePresentationControl // MARK: UIViewController Lifecycle + override public func viewWillAppear(_ animated: Bool) { + view.backgroundColor = ShopifyCheckoutSheetKit.configuration.backgroundColor + } + override public func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = ShopifyCheckoutSheetKit.configuration.backgroundColor - view.addSubview(checkoutView) NSLayoutConstraint.activate([ checkoutView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), diff --git a/Sources/ShopifyCheckoutSheetKit/IndeterminateProgressBarView.swift b/Sources/ShopifyCheckoutSheetKit/IndeterminateProgressBarView.swift index d28ab196..bdbb20a3 100644 --- a/Sources/ShopifyCheckoutSheetKit/IndeterminateProgressBarView.swift +++ b/Sources/ShopifyCheckoutSheetKit/IndeterminateProgressBarView.swift @@ -17,10 +17,10 @@ class IndeterminateProgressBarView: UIView { NSLayoutConstraint.activate([ progressBar.topAnchor.constraint(equalTo: topAnchor), - progressBar.heightAnchor.constraint(equalToConstant: 4), + progressBar.heightAnchor.constraint(equalToConstant: 1), ]) - progressBar.tintColor = .systemGray5 + progressBar.tintColor = ShopifyCheckoutSheetKit.configuration.spinnerColor } override func didMoveToSuperview() { @@ -43,7 +43,6 @@ class IndeterminateProgressBarView: UIView { } func startAnimating() { - print("start animating") alpha = 1 isHidden = false }