Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow AccountSetup to be created with async function to handle completeness of the account setup #73

Merged
merged 24 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
84 changes: 46 additions & 38 deletions Sources/SpeziAccount/AccountSetup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import OrderedCollections
import SpeziViews
import SwiftUI


Expand Down Expand Up @@ -66,7 +67,7 @@ public enum _AccountSetupState: EnvironmentKey, Sendable { // swiftlint:disable:
/// - ``DefaultAccountSetupHeader``
@MainActor
public struct AccountSetup<Header: View, Continue: View>: View {
private let setupCompleteClosure: (AccountDetails) -> Void
private let setupCompleteClosure: (AccountDetails) async -> Void
Supereg marked this conversation as resolved.
Show resolved Hide resolved
private let header: Header
private let continueButton: Continue

Expand All @@ -78,6 +79,7 @@ public struct AccountSetup<Header: View, Continue: View>: View {
@State private var setupState: _AccountSetupState = .generic
@State private var compliance: SignupProviderCompliance?
@State private var followUpSheet = false
@State private var isCompletingSetup = false

private var hasSetupComponents: Bool {
account.accountSetupComponents.contains { $0.configuration.isEnabled }
Expand All @@ -86,37 +88,7 @@ public struct AccountSetup<Header: View, Continue: View>: View {
public var body: some View {
GeometryReader { proxy in
ScrollView(.vertical) {
VStack {
if hasSetupComponents {
header
.environment(\._accountSetupState, setupState)
}

Spacer()

if let details = account.details, !details.isAnonymous {
switch setupState {
case let .requiringAdditionalInfo(keys):
followUpInformationSheet(details, requiredKeys: keys)
case .loadingExistingAccount:
// We allow the outer view to navigate away upon signup, before we show the existing account view
existingAccountLoading
default:
ExistingAccountView(details: details) {
continueButton
}
}
} else {
accountSetupView
.onAppear {
setupState = .setupShown
}
}

Spacer()
Spacer()
Spacer()
}
scrollableContentView
.padding(.horizontal, ViewSizing.outerHorizontalPadding)
.frame(minHeight: proxy.size.height)
.frame(maxWidth: .infinity)
Expand All @@ -128,10 +100,44 @@ public struct AccountSetup<Header: View, Continue: View>: View {
case .setupShown = setupState else {
return
}

handleSuccessfulSetup(details)
}
}

@ViewBuilder private var scrollableContentView: some View {
VStack {
if hasSetupComponents {
header
.environment(\._accountSetupState, setupState)
}
Spacer()
if let details = account.details, !details.isAnonymous {
switch setupState {
case let .requiringAdditionalInfo(keys):
followUpInformationSheet(details, requiredKeys: keys)
case .loadingExistingAccount:
// We allow the outer view to navigate away upon signup, before we show the existing account view
existingAccountLoading
pauljohanneskraft marked this conversation as resolved.
Show resolved Hide resolved
default:
if isCompletingSetup {
ProgressView()
} else {
ExistingAccountView(details: details) {
continueButton
}
}
}
} else {
accountSetupView
.onAppear {
setupState = .setupShown
}
}
Spacer()
Spacer()
Spacer()
}
}

@ViewBuilder private var accountSetupView: some View {
if !hasSetupComponents {
Expand Down Expand Up @@ -189,7 +195,7 @@ public struct AccountSetup<Header: View, Continue: View>: View {
/// - continue: A custom continue button you can place. This view will be rendered if the AccountSetup view is
/// displayed with an already associated account.
public init(
setupComplete: @escaping (AccountDetails) -> Void = { _ in },
setupComplete: @escaping (AccountDetails) async -> Void = { _ in },
Supereg marked this conversation as resolved.
Show resolved Hide resolved
@ViewBuilder header: () -> Header = { DefaultAccountSetupHeader() },
@ViewBuilder `continue`: () -> Continue = { EmptyView() }
) {
Expand All @@ -212,8 +218,7 @@ public struct AccountSetup<Header: View, Continue: View>: View {
}
.onChange(of: followUpSheet) {
if !followUpSheet { // follow up information was completed!
setupState = .loadingExistingAccount
setupCompleteClosure(details)
handleSetupCompleted(details)
}
}
}
Expand Down Expand Up @@ -252,8 +257,11 @@ public struct AccountSetup<Header: View, Continue: View>: View {
}

private func handleSetupCompleted(_ details: AccountDetails) {
setupState = .loadingExistingAccount
setupCompleteClosure(details)
isCompletingSetup = true
Task { @MainActor in
pauljohanneskraft marked this conversation as resolved.
Show resolved Hide resolved
await setupCompleteClosure(details)
isCompletingSetup = false
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziAccount/Environment/AccountRequiredKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
import SwiftUI


struct AccountRequiredKey: EnvironmentKey {
private struct AccountRequiredKey: EnvironmentKey {
static let defaultValue = false
}


extension EnvironmentValues {
/// An environment variable that indicates if an account was configured to be required for the app.
///
/// Fore more information have a look at ``SwiftUI/View/accountRequired(_:considerAnonymousAccounts:setupSheet:)``.
/// Fore more information have a look at ``SwiftUI/View/accountRequired(_:isValid:setupSheet:)``.
Supereg marked this conversation as resolved.
Show resolved Hide resolved
public var accountRequired: Bool {
get {
self[AccountRequiredKey.self]
Expand Down
6 changes: 6 additions & 0 deletions Sources/SpeziAccount/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,9 @@
}
}
}
},
"Date of Birth" : {

},
"DELETE" : {
"localizations" : {
Expand Down Expand Up @@ -664,6 +667,9 @@
}
}
}
},
"E-Mail and Password" : {

},
"EDIT" : {
"localizations" : {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziAccount/SpeziAccount.docc/SpeziAccount.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Refer to the <doc:Creating-your-own-Account-Service> article if you plan on impl
- ``AccountOverview``
- ``AccountHeader``
- ``FollowUpInfoSheet``
- ``SwiftUI/View/accountRequired(_:considerAnonymousAccounts:setupSheet:)``
- ``SwiftUI/View/accountRequired(_:isValid:setupSheet:)``
Supereg marked this conversation as resolved.
Show resolved Hide resolved
- ``SwiftUI/EnvironmentValues/accountRequired``

### Environment & Preferences
Expand Down
20 changes: 12 additions & 8 deletions Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@
import SwiftUI


private let logger = Logger(subsystem: "edu.stanford.sepzi.SepziAccount", category: "AccountRequiredModifier")

Check warning on line 13 in Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package / Test using xcodebuild or run fastlane

let 'logger' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6

Check warning on line 13 in Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package / Test using xcodebuild or run fastlane

let 'logger' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6

Check warning on line 13 in Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package visionOS / Test using xcodebuild or run fastlane

let 'logger' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6

Check warning on line 13 in Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests / Test using xcodebuild or run fastlane

let 'logger' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6

Check warning on line 13 in Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests / Test using xcodebuild or run fastlane

let 'logger' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6

Check warning on line 13 in Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests visionOS / Test using xcodebuild or run fastlane

let 'logger' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6

Check warning on line 13 in Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests visionOS / Test using xcodebuild or run fastlane

let 'logger' is not concurrency-safe because it is not either conforming to 'Sendable' or isolated to a global actor; this is an error in Swift 6


struct AccountRequiredModifier<SetupSheet: View>: ViewModifier {
private let enabled: Bool
private let isValid: (AccountDetails) -> Bool
private let setupSheet: SetupSheet
private let considerAnonymousAccounts: Bool

@Environment(Account.self)
private var account: Account? // make sure that the modifier can be used when account is not configured
Expand All @@ -32,15 +32,19 @@
return true // not signedIn
}

// we present the sheet if the account is anonymous and we do not consider anonymous accounts to the fully signed in
return details.isAnonymous && !considerAnonymousAccounts
// we present the sheet if the account is not valid (yet)
return !isValid(details)
}


init(enabled: Bool, considerAnonymousAccounts: Bool, @ViewBuilder setupSheet: () -> SetupSheet) {
init(
enabled: Bool,
isValid: @escaping (AccountDetails) -> Bool,
pauljohanneskraft marked this conversation as resolved.
Show resolved Hide resolved
@ViewBuilder setupSheet: () -> SetupSheet
) {
self.enabled = enabled
self.isValid = isValid
self.setupSheet = setupSheet()
self.considerAnonymousAccounts = considerAnonymousAccounts
}


Expand All @@ -58,7 +62,7 @@

guard account != nil else {
logger.error("""
accountRequired(_:considerAnonymousAccounts:setupSheet:) modifier was enabled but `Account` was not configured. \
accountRequired(_:isValid:setupSheet:) modifier was enabled but `Account` was not configured. \
Supereg marked this conversation as resolved.
Show resolved Hide resolved
Make sure to include the `AccountConfiguration` the configuration section of your App delegate.
""")
return
Expand Down Expand Up @@ -94,9 +98,9 @@
/// - Returns: The modified view.
public func accountRequired<SetupSheet: View>(
_ required: Bool = true,
considerAnonymousAccounts: Bool = false,
Supereg marked this conversation as resolved.
Show resolved Hide resolved
isValid: @escaping (AccountDetails) -> Bool = { !$0.isAnonymous },
@ViewBuilder setupSheet: () -> SetupSheet
) -> some View {
modifier(AccountRequiredModifier(enabled: required, considerAnonymousAccounts: considerAnonymousAccounts, setupSheet: setupSheet))
modifier(AccountRequiredModifier(enabled: required, isValid: isValid, setupSheet: setupSheet))
}
}
Loading