Skip to content

Commit

Permalink
[Sample: MobileBuyIntegration] Handle Universal Links
Browse files Browse the repository at this point in the history
  • Loading branch information
markmur committed Nov 13, 2024
1 parent e64d89a commit 3ea8a1e
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "# Remove .pcm binaries from build\nfind \"${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}\" -name \"*.pcm\" -type f -delete\n\n# Remove Swiftlint binaries from build\nfind \"${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}\" -name \"SwiftLint*\" -type f -delete\n\n\n";
shellScript = "# Remove .pcm binaries from build\nfind \"${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}\" -name \"*.pcm\" -type f -delete\n\n# Remove Swiftlint binaries from build\nfind \"${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}\" -iname \"swiftlint*\" -type f -delete\n\n\n\n";
};
/* End PBXShellScriptBuildPhase section */

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,14 @@ 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 Foundation
import ShopifyCheckoutSheetKit

public struct AppConfiguration {
public var storefrontDomain: String = Bundle.main.infoDictionary?["StorefrontDomain"] as? String ?? ""

public var universalLinks = UniversalLinks()

/// Prefill buyer information
public var useVaultedState: Bool = false

Expand All @@ -36,3 +41,8 @@ public var appConfiguration = AppConfiguration() {
CartManager.shared.resetCart()
}
}

public struct UniversalLinks {
public var handleCheckoutInApp: Bool = true
public var handleAllURLsInApp: Bool = true
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
},
"App version" : {

},
"By default, the app will only handle Checkout URLs (non-thank you page) and route everything else to Safari. This setting will route all Universal Links to the app." : {

},
"Clear" : {

Expand All @@ -18,6 +21,12 @@
},
"Features" : {

},
"Handle all Universal Links" : {

},
"Handle Checkout URLs" : {

},
"Logs" : {

Expand Down Expand Up @@ -66,6 +75,9 @@
},
"Theme" : {

},
"Universal Links" : {

},
"Version" : {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import ShopifyCheckoutSheetKit
class ProductViewController: UIViewController {

// MARK: Properties
private var handle: String?

@IBOutlet private var image: UIImageView!

Expand Down Expand Up @@ -55,8 +56,10 @@ class ProductViewController: UIViewController {

// MARK: Initializers

convenience init() {
convenience init(handle: String?) {
self.init(nibName: nil, bundle: nil)

self.handle = handle
}

// MARK: UIViewController Lifecycle
Expand All @@ -80,7 +83,11 @@ class ProductViewController: UIViewController {
}
}

reloadProduct()
if let handle = handle {
getProductByHandle(handle)
} else {
reloadProduct()
}
}

private func rerender() {
Expand All @@ -89,6 +96,7 @@ class ProductViewController: UIViewController {
$0.configuration?.showsActivityIndicator = self.loading
}
}
updateProductDetails()
}

private func setLoading(_ state: Bool) {
Expand All @@ -101,6 +109,13 @@ class ProductViewController: UIViewController {
}
}

private func setProduct(_ product: Storefront.Product?) {
if let product = product {
self.product = product
self.handle = product.handle
}
}

// MARK: Actions

@IBAction func addToCart() {
Expand All @@ -117,12 +132,49 @@ class ProductViewController: UIViewController {
}
}

public func getProductByHandle(_ handle: String) {
let context = Storefront.InContextDirective(country: Storefront.CountryCode.inferRegion())
let query = Storefront.buildQuery(inContext: context) { $0
.products(first: 1, query: handle) { $0
.nodes { $0
.id()
.title()
.handle()
.description()
.vendor()
.featuredImage { $0
.url()
}
.variants(first: 1) { $0
.nodes { $0
.id()
.title()
.availableForSale()
.price { $0
.amount()
.currencyCode()
}
}
}
}
}
}

StorefrontClient.shared.execute(query: query) { [weak self] result in
self?.setLoading(false)
if case .success(let query) = result {
self?.setProduct(query.products.nodes.first)
}
}
}

@IBAction private func reloadProduct() {
let query = Storefront.buildQuery(inContext: Storefront.InContextDirective(country: Storefront.CountryCode.inferRegion())) { $0
.products(first: 250) { $0
.nodes { $0
.id()
.title()
.handle()
.description()
.vendor()
.featuredImage { $0
Expand All @@ -146,7 +198,7 @@ class ProductViewController: UIViewController {
StorefrontClient.shared.execute(query: query) { [weak self] result in
self?.setLoading(false)
if case .success(let query) = result {
self?.product = query.products.nodes.randomElement()
self?.setProduct(query.products.nodes.randomElement())
}
}
}
Expand Down
124 changes: 114 additions & 10 deletions Samples/MobileBuyIntegration/MobileBuyIntegration/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,32 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {

var window: UIWindow?

var cartController: CartViewController?
var productController: ProductViewController?

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }

let tabBarController = UITabBarController()

/// Catalog
let catalogController = ProductViewController()
catalogController.tabBarItem.image = UIImage(systemName: "books.vertical")
catalogController.tabBarItem.title = "Browse"
catalogController.navigationItem.title = "Product details"
productController = ProductViewController()
productController?.tabBarItem.image = UIImage(systemName: "books.vertical")
productController?.tabBarItem.title = "Browse"
productController?.navigationItem.title = "Product details"

/// Cart
let cartController = CartViewController()
cartController.tabBarItem.image = UIImage(systemName: "cart")
cartController.tabBarItem.title = "Cart"
cartController.navigationItem.title = "Cart"
cartController = CartViewController()
cartController?.tabBarItem.image = UIImage(systemName: "cart")
cartController?.tabBarItem.title = "Cart"
cartController?.navigationItem.title = "Cart"

tabBarController.viewControllers = [
UINavigationController(
rootViewController: catalogController
rootViewController: productController!
),
UINavigationController(
rootViewController: cartController
rootViewController: cartController!
)
]

Expand All @@ -76,6 +79,66 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
self.window = window
}

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL else {
return
}

handleUniversalLink(url: incomingURL)
}

func handleUniversalLink(url: URL) {
// The URL host must match the StorefrontDomain defined in our env
guard let host = url.host, host == appConfiguration.storefrontDomain else { return }

let storefrontUrl = StorefrontURL(from: url)

switch true {
case appConfiguration.universalLinks.handleCheckoutInApp && storefrontUrl.isCheckout() && !storefrontUrl.isThankYouPage():
if let vc = cartController {
ShopifyCheckoutSheetKit.present(checkout: url, from: vc, delegate: vc)
}
case appConfiguration.universalLinks.handleAllURLsInApp:
if storefrontUrl.isCart() {
navigateToCart()
} else if let slug = storefrontUrl.getProductSlug() {
navigateToProduct(with: slug)
}
default:
// Open all other links in Safari
UIApplication.shared.open(url)
}
}

private func getRootViewController() -> UINavigationController? {
return window?.rootViewController as? UINavigationController
}

private func getNavigationController(forTab index: Int) -> UINavigationController? {
guard let tabBarVC = window?.rootViewController as? UITabBarController else {
return nil
}
return tabBarVC.viewControllers?[index] as? UINavigationController
}

func navigateToCart() {
if let tabBarVC = window?.rootViewController as? UITabBarController {
tabBarVC.selectedIndex = 1
}
}


func navigateToProduct(with handle: String) {
if let pdp = self.productController {
pdp.getProductByHandle(handle)
}

if let tabBarVC = window?.rootViewController as? UITabBarController {
tabBarVC.selectedIndex = 0
}
}

@objc func colorSchemeChanged() {
window?.overrideUserInterfaceStyle = ShopifyCheckoutSheetKit.configuration.colorScheme.userInterfaceStyle
}
Expand All @@ -97,3 +160,44 @@ extension Configuration.ColorScheme {
}
}
}

public struct StorefrontURL {
public let url: URL

private let slug = "([\\w\\d_-]+)"

init(from url: URL) {
self.url = url
}

public func isThankYouPage() -> Bool {
return url.path.range(of: "/thank[-_]you", options: .regularExpression) != nil
}

public func isCheckout() -> Bool {
return url.path.contains("/checkout")
}

public func isCart() -> Bool {
return url.path.contains("/cart")
}

public func isCollection() -> Bool {
return url.path.range(of: "/collections/\(slug)", options: .regularExpression) != nil
}

public func isProduct() -> Bool {
return url.path.range(of: "/products/\(slug)", options: .regularExpression) != nil
}

public func getProductSlug() -> String? {
guard isProduct() else { return nil }

let pattern = "/products/([\\w_-]+)"
if let match = url.path.range(of: pattern, options: .regularExpression, range: nil, locale: nil) {
let slug = url.path[match].components(separatedBy: "/").last
return slug
}
return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import ShopifyCheckoutSheetKit
struct SettingsView: View {
@State private var preloadingEnabled = ShopifyCheckoutSheetKit.configuration.preloading.enabled
@State private var useVaultedState = appConfiguration.useVaultedState
@State private var handleCheckoutInApp = appConfiguration.universalLinks.handleCheckoutInApp
@State private var handleAllURLsInApp = appConfiguration.universalLinks.handleAllURLsInApp
@State private var logs: [String?] = LogReader.shared.readLogs() ?? []
@State private var selectedColorScheme = ShopifyCheckoutSheetKit.configuration.colorScheme
@State private var colorScheme: ColorScheme = .light
Expand All @@ -47,6 +49,20 @@ struct SettingsView: View {
}
}

Section(header: Text("Universal Links")) {
Toggle("Handle Checkout URLs", isOn: $handleCheckoutInApp)
.onChange(of: handleCheckoutInApp) { newValue in
appConfiguration.universalLinks.handleCheckoutInApp = newValue
}
Toggle("Handle all Universal Links", isOn: $handleAllURLsInApp)
.onChange(of: handleAllURLsInApp) { newValue in
appConfiguration.universalLinks.handleAllURLsInApp = newValue
}

Text("By default, the app will only handle Checkout URLs (non-thank you page) and route everything else to Safari. This setting will route all Universal Links to the app.")
.font(.caption)
}

Section(header: Text("Theme")) {
ForEach(Configuration.ColorScheme.allCases, id: \.self) { scheme in
ColorSchemeView(scheme: scheme, isSelected: scheme == selectedColorScheme)
Expand Down

0 comments on commit 3ea8a1e

Please sign in to comment.