Skip to content

Commit

Permalink
accountRequired modifier should not consider anonymous accounts by …
Browse files Browse the repository at this point in the history
…default. (#72)

# `accountRequired` modifier should not consider anonymous accounts by
default.

## ♻️ Current situation & Problem
Currently, the `accountRequired(_:setupSheet:)` modifier considers any
associated account details as a valid account and therefore dismisses
the provided setup sheet. However, typically anonymous accounts are not
considered full accounts and are only used to have a persistent account
identifier. The `AccountSetup` sheet treats anonymous accounts similarly
where it still shows all the signup options.

This PR introduces a new flag passed to the
`accountRequired(_:considerAnonymousAccounts:setupSheet:)` modifier to
control the behavior of anonymous accounts. By default anonymous
accounts are treated the same as if no account was present.

## ⚙️ Release Notes 
* Ignore anonymous accounts with the `accountRequired` modifier by
default.

## 📚 Documentation
Documentation was updated.


## ✅ Testing
--

## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
Supereg authored Sep 1, 2024
1 parent b9c1cb6 commit 7e78ee9
Show file tree
Hide file tree
Showing 8 changed files with 39 additions and 25 deletions.
2 changes: 1 addition & 1 deletion Sources/SpeziAccount/Environment/AccountRequiredKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct AccountRequiredKey: EnvironmentKey {
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(_:setupSheet:)``.
/// Fore more information have a look at ``SwiftUI/View/accountRequired(_:considerAnonymousAccounts:setupSheet:)``.
public var accountRequired: Bool {
get {
self[AccountRequiredKey.self]
Expand Down
4 changes: 2 additions & 2 deletions Sources/SpeziAccount/ExternalAccountStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import Spezi
/// - ``updatedDetails``
/// - ``requestExternalStorage(of:for:)``
/// - ``updateExternalStorage(with:for:)``
/// - ``retrieveExternalStorage(for:_:)-8gbg``
/// - ``retrieveExternalStorage(for:_:)-8zvkq``
/// - ``retrieveExternalStorage(for:_:)-5ngpm``
/// - ``retrieveExternalStorage(for:_:)-1x0ps``
///
/// ### Communicate changes as a Storage Provider
/// - ``notifyAboutUpdatedDetails(for:_:)``
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziAccount/Mock/InMemoryAccountService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ public final class InMemoryAccountService: AccountService {
unsupportedKeys.removeAll(Self.supportedKeys)
if !unsupportedKeys.isEmpty {
let externalStorage = externalStorage
let externallyStored = try await externalStorage.retrieveExternalStorage(for: user.accountId.uuidString, unsupportedKeys)
let externallyStored = await externalStorage.retrieveExternalStorage(for: user.accountId.uuidString, unsupportedKeys)
details.add(contentsOf: externallyStored)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,6 @@ Make sure to implement your `AccountService` the following way
* Make sure to subscribe to updates from the storage provider using the `AsyncStream` ``ExternalAccountStorage/updatedDetails``.
* Create a new record by calling ``ExternalAccountStorage/requestExternalStorage(of:for:)``.
* Update externally stored details by calling ``ExternalAccountStorage/updateExternalStorage(with:for:)``.
* Retrieve externally stored details by calling ``ExternalAccountStorage/retrieveExternalStorage(for:_:)-8gbg``
* Retrieve externally stored details by calling ``ExternalAccountStorage/retrieveExternalStorage(for:_:)-5ngpm``

> Note: Refer to the documentation of ``ExternalAccountStorage`` for more information.
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(_:setupSheet:)``
- ``SwiftUI/View/accountRequired(_:considerAnonymousAccounts:setupSheet:)``
- ``SwiftUI/EnvironmentValues/accountRequired``

### Environment & Preferences
Expand Down
46 changes: 29 additions & 17 deletions Sources/SpeziAccount/ViewModifier/AccountRequiredModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,49 +16,56 @@ private let logger = Logger(subsystem: "edu.stanford.sepzi.SepziAccount", catego
struct AccountRequiredModifier<SetupSheet: View>: ViewModifier {
private let enabled: 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

@State private var presentingSheet = false

@MainActor private var shouldPresentSheet: Bool {
guard let account, enabled else {
return false
}

init(enabled: Bool, @ViewBuilder setupSheet: () -> SetupSheet) {
guard let details = account.details else {
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
}


init(enabled: Bool, considerAnonymousAccounts: Bool, @ViewBuilder setupSheet: () -> SetupSheet) {
self.enabled = enabled
self.setupSheet = setupSheet()
self.considerAnonymousAccounts = considerAnonymousAccounts
}


func body(content: Content) -> some View {
content
.onChange(of: [account?.signedIn, presentingSheet]) {
guard enabled, let account else {
return
}

if !account.signedIn {
if !presentingSheet {
presentingSheet = true
}
} else {
presentingSheet = false
.onChange(of: [shouldPresentSheet, presentingSheet]) {
if shouldPresentSheet != presentingSheet {
presentingSheet = shouldPresentSheet
}
}
.task {
guard enabled else {
return
}

guard let account else {
guard account != nil else {
logger.error("""
accountRequired(_:setupSheet:) modifier was enabled but `Account` was not configured. \
accountRequired(_:considerAnonymousAccounts:setupSheet:) modifier was enabled but `Account` was not configured. \
Make sure to include the `AccountConfiguration` the configuration section of your App delegate.
""")
return
}

try? await Task.sleep(for: .seconds(2))
if !account.signedIn {
if shouldPresentSheet {
presentingSheet = true
}
}
Expand All @@ -81,10 +88,15 @@ extension View {
///
/// - Parameters:
/// - required: The flag indicating if an account is required at all times.
/// - considerAnonymousAccounts: Anonymous accounts are considered full accounts and fulfill the account requirements. See ``AccountDetails/isAnonymous``.
/// - setupSheet: The view that is presented if no account was detected. You may present the ``AccountSetup`` view here.
/// This view is directly used with the standard SwiftUI sheet modifier.
/// - Returns: The modified view.
public func accountRequired<SetupSheet: View>(_ required: Bool = true, @ViewBuilder setupSheet: () -> SetupSheet) -> some View {
modifier(AccountRequiredModifier(enabled: required, setupSheet: setupSheet))
public func accountRequired<SetupSheet: View>(
_ required: Bool = true,
considerAnonymousAccounts: Bool = false,
@ViewBuilder setupSheet: () -> SetupSheet
) -> some View {
modifier(AccountRequiredModifier(enabled: required, considerAnonymousAccounts: considerAnonymousAccounts, setupSheet: setupSheet))
}
}
4 changes: 3 additions & 1 deletion Tests/UITests/TestAppUITests/AccountOverviewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,9 @@ final class AccountOverviewTests: XCTestCase { // swiftlint:disable:this type_bo
#if !os(visionOS)
try app.textFields["E-Mail Address"].delete(count: 12, options: [.disableKeyboardDismiss, .tapFromRight])
#else
try app.textFields["E-Mail Address"].delete(count: 12, options: [.disableKeyboardDismiss])
// on visionOS we tap the cursor after the dot. We just split it up into two 6 character deletes
try app.textFields["E-Mail Address"].delete(count: 6, options: [.disableKeyboardDismiss])
try app.textFields["E-Mail Address"].delete(count: 6, options: [.disableKeyboardDismiss])
#endif

// failed validation
Expand Down
2 changes: 1 addition & 1 deletion Tests/UITests/TestAppUITests/AccountSetupTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ final class AccountSetupTests: XCTestCase { // swiftlint:disable:this type_body_
#endif

// we access the signup button through the collectionView as there is another signup button behind the signup sheet.
XCTAssertTrue(app.collectionViews.buttons["Signup"].exists)
XCTAssertTrue(app.collectionViews.buttons["Signup"].waitForExistence(timeout: 2.0))
app.collectionViews.buttons["Signup"].tap()

XCTAssertTrue(app.staticTexts[email].waitForExistence(timeout: 4.0))
Expand Down

0 comments on commit 7e78ee9

Please sign in to comment.