Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
badgeLabel.isHidden = count <= 0
}

func scene(_: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }

if CustomerAccountManager.shared.handleCallback(url: url) {
return
}
}

func scene(_: UIScene, continue userActivity: NSUserActivity) {
guard
userActivity.activityType == NSUserActivityTypeBrowsingWeb,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,14 @@ final class KeychainHelper {
return
}
save(key: emailKey, data: data)
logger.debug("Saved email to keychain: \(email)")
logger.debug("Saved email to keychain")
}

func getEmail() -> String? {
guard let data = read(key: emailKey) else {
logger.debug("No email found in keychain")
return nil
}
let email = String(data: data, encoding: .utf8)
logger.debug("Retrieved email from keychain: \(email ?? "nil")")
return email
return String(data: data, encoding: .utf8)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,6 @@
}
}
},
"Sign In" : {
"comment" : "The title of the navigation bar at the top of the view.",
"isCommentAutoGenerated" : true
},
"Sign in on the" : {
"comment" : "A phrase displayed in a navigation link within the Settings view, directing the user to the Shopify account tab.",
"isCommentAutoGenerated" : true
Expand All @@ -109,10 +105,6 @@
"comment" : "A label describing the action of signing in to access one's account.",
"isCommentAutoGenerated" : true
},
"Sign Out" : {
"comment" : "A button label that allows a user to sign out of their account.",
"isCommentAutoGenerated" : true
},
"Signed In" : {
"comment" : "A title displayed in the top-center of the view.",
"isCommentAutoGenerated" : true
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,82 @@
import AuthenticationServices
import SwiftUI
import UIKit

struct AccountView: View {
@ObservedObject var accountManager = CustomerAccountManager.shared
@State private var showingLogin = false
@State private var errorMessage = ""
@State private var showingError = false

var body: some View {
NavigationView {
Group {
if accountManager.isAuthenticated {
AuthenticatedAccountView()
AuthenticatedAccountView(logout: logout)
} else {
UnauthenticatedAccountView(showingLogin: $showingLogin)
UnauthenticatedAccountView(signIn: signIn)
}
}
.navigationTitle(accountManager.isAuthenticated ? "Account" : "Sign In")
}
.sheet(isPresented: $showingLogin) {
LoginSheetView()
.alert("Customer Account", isPresented: $showingError) {
Button("OK", role: .cancel) {}
} message: {
Text(errorMessage)
}
}

private func signIn() {
guard let presentationAnchor = UIApplication.shared.customerAccountPresentationAnchor else {
presentError("Unable to find a window for sign in.")
return
}

Task {
do {
try await accountManager.signIn(from: presentationAnchor)
} catch CustomerAccountError.authorizationCancelled {
return
} catch {
presentError(error.localizedDescription)
}
}
}

private func logout() {
guard let presentationAnchor = UIApplication.shared.customerAccountPresentationAnchor else {
accountManager.logout()
return
}

Task {
do {
try await accountManager.logout(from: presentationAnchor)
} catch {
presentError(
"You were signed out of this app, but the browser session may still be active. \(error.localizedDescription)"
)
}
}
}

private func presentError(_ message: String) {
errorMessage = message
showingError = true
}
}

extension UIApplication {
fileprivate var customerAccountPresentationAnchor: ASPresentationAnchor? {
connectedScenes
.compactMap { $0 as? UIWindowScene }
.first { $0.activationState == .foregroundActive }?
.keyWindow
}
}

struct AuthenticatedAccountView: View {
@ObservedObject var accountManager = CustomerAccountManager.shared
let logout: () -> Void

var body: some View {
VStack(spacing: 24) {
Expand Down Expand Up @@ -53,23 +107,25 @@ struct AuthenticatedAccountView: View {

Spacer()

Button(action: { accountManager.logout() }) {
Button(action: logout) {
HStack {
Image(systemName: "rectangle.portrait.and.arrow.right")
Text("Sign Out")
Text(accountManager.isLoading ? "Signing Out…" : "Sign Out")
}
.foregroundColor(.red)
.frame(maxWidth: .infinity)
.padding()
}
.disabled(accountManager.isLoading)
.padding(.bottom, 32)
}
.background(Color(.systemGroupedBackground))
}
}

struct UnauthenticatedAccountView: View {
@Binding var showingLogin: Bool
@ObservedObject var accountManager = CustomerAccountManager.shared
let signIn: () -> Void

var body: some View {
VStack(spacing: 24) {
Expand All @@ -91,17 +147,16 @@ struct UnauthenticatedAccountView: View {
.padding(.horizontal, 32)
}

Button(action: {
showingLogin = true
}) {
Text("Sign In")
Button(action: signIn) {
Text(accountManager.isLoading ? "Signing In…" : "Sign In")
.fontWeight(.semibold)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.padding()
.background(Color(ColorPalette.primaryColor))
.cornerRadius(12)
}
.disabled(accountManager.isLoading)
.padding(.horizontal, 32)

Spacer()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,139 +1,18 @@
import SwiftUI
import WebKit

struct CustomerAccountLoginView: UIViewRepresentable {
let authorizationURL: URL
let callbackScheme: String
let onCodeReceived: (String) -> Void
let onCancel: () -> Void

func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = .nonPersistent()

let webView = WKWebView(frame: .zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
webView.load(URLRequest(url: authorizationURL))
return webView
}

func updateUIView(_: WKWebView, context _: Context) {}

func makeCoordinator() -> Coordinator {
Coordinator(callbackScheme: callbackScheme, onCodeReceived: onCodeReceived, onCancel: onCancel)
}

class Coordinator: NSObject, WKNavigationDelegate {
let callbackScheme: String
let onCodeReceived: (String) -> Void
let onCancel: () -> Void

init(callbackScheme: String, onCodeReceived: @escaping (String) -> Void, onCancel: @escaping () -> Void) {
self.callbackScheme = callbackScheme
self.onCodeReceived = onCodeReceived
self.onCancel = onCancel
}

func webView(
_: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: @escaping @MainActor @Sendable (WKNavigationActionPolicy) -> Void
) {
guard let url = navigationAction.request.url else {
decisionHandler(.allow)
return
}

if url.scheme == callbackScheme, url.host == "callback" {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems
else {
decisionHandler(.cancel)
onCancel()
return
}

let queryDict = Dictionary(uniqueKeysWithValues: queryItems.compactMap { item -> (String, String)? in
guard let value = item.value else { return nil }
return (item.name, value)
})

if let code = queryDict["code"] {
onCodeReceived(code)
} else {
onCancel()
}

decisionHandler(.cancel)
return
}

decisionHandler(.allow)
}

func webView(_: WKWebView, didFail _: WKNavigation!, withError error: Error) {
if (error as NSError).code == NSURLErrorCancelled {
return
}
print("WebView navigation failed: \(error)")
}

func webView(_: WKWebView, didFailProvisionalNavigation _: WKNavigation!, withError error: Error) {
if (error as NSError).code == NSURLErrorCancelled {
return
}
print("WebView provisional navigation failed: \(error)")
}
}
}

struct LoginSheetView: View {
@ObservedObject var accountManager = CustomerAccountManager.shared
@Environment(\.dismiss) private var dismiss
@State private var authorizationURL: URL?

private var callbackScheme: String? {
guard let shopId = InfoDictionary.shared.customerAccountApiShopId else { return nil }
return "shop.\(shopId).app"
import AuthenticationServices

/// Retains the host-provided window while an authentication session is active.
///
/// Authentication UI is owned by `ASWebAuthenticationSession`; the demo no
/// longer presents a login `WKWebView`.
@MainActor
final class CustomerAccountPresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
private let anchor: ASPresentationAnchor

init(anchor: ASPresentationAnchor) {
self.anchor = anchor
}

var body: some View {
NavigationView {
Group {
if let url = authorizationURL, let scheme = callbackScheme {
CustomerAccountLoginView(
authorizationURL: url,
callbackScheme: scheme,
onCodeReceived: { code in
Task {
do {
try await accountManager.exchangeCodeForTokens(code: code)
dismiss()
} catch {
print("Failed to exchange code: \(error)")
}
}
},
onCancel: {
dismiss()
}
)
} else {
ProgressView("Loading...")
}
}
.navigationTitle("Sign In")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
}
}
.onAppear {
authorizationURL = accountManager.buildAuthorizationURL()
}
func presentationAnchor(for _: ASWebAuthenticationSession) -> ASPresentationAnchor {
anchor
}
}
Loading
Loading