Skip to content

Commit

Permalink
Rework AccountValue model, implement basic configuration update TestApp
Browse files Browse the repository at this point in the history
  • Loading branch information
Supereg committed Jul 3, 2023
1 parent af9636a commit 4c84149
Show file tree
Hide file tree
Showing 44 changed files with 481 additions and 402 deletions.
65 changes: 40 additions & 25 deletions Sources/SpeziAccount/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,58 @@ import SwiftUI
/// TODO update docs!
///
/// The ``Account/Account`` type also enables interaction with the ``AccountService``s from anywhere in the view hierarchy.
public actor Account: ObservableObject {
@MainActor
public class Account: ObservableObject {
/// The ``Account/Account/signedIn`` determines if the the current Account context is signed in or not yet signed in.
@MainActor
public var signedIn: Bool {
account != nil
}

@MainActor
@Published
public private(set) var account: AccountValuesWhat? // TODO UserAccount/User! TODO must only be accessible/modifieable through an AccountService!
@Published public var signedIn = false
@Published public private(set) var user: UserInfo? // TODO must only be accessible/modifieable through an AccountService!
@Published public private(set) var activeAccountService: (any AccountService)?

// TODO how to get to the account service that holds the active account?

// TODO make a configuration objet, where all other account services may enter themselves!

/// An account provides a collection of ``AccountService``s that are used to populate login, sign up, or reset password screens.
nonisolated let accountServices: [any AccountService]



/// - Parameter accountServices: An account provides a collection of ``AccountService``s that are used to populate login, sign up, or reset password screens.
public init(accountServices: [any AccountService]) {
self.accountServices = accountServices
// TODO for accountService in accountServices {
// accountService.inject(account: self)
// }
/// - Parameter services: An account provides a collection of ``AccountService``s that are used to populate login, sign up, or reset password screens.
public nonisolated init(services: [any AccountService] = []) {
self.accountServices = services
for service in services {
service.inject(account: self)
}
}

init(accountServices: [any AccountService] = [], account: AccountValuesWhat) {
self.accountServices = accountServices
self._account = Published(wrappedValue: account)
/// Initializer useful for testing and previewing purposes.
nonisolated init(account: UserInfo, active accountService: any AccountService) {
self.accountServices = [accountService]
self._user = Published(wrappedValue: account)
self._activeAccountService = Published(wrappedValue: accountService)

accountService.inject(account: self)
}
}

public struct AccountValuesWhat: Sendable, ModifiableAccountValueStorageContainer { // TODO naming is off!
public var storage: AccountValueStorage
public func supplyUserInfo<Service: AccountService>(_ user: UserInfo, by accountService: Service) {
if let activeAccountService {
precondition(ObjectIdentifier(accountService) == ObjectIdentifier(activeAccountService)) // TODO message
}

self.activeAccountService = accountService
self.user = user
if !signedIn {
signedIn = true
}
}

public init(storage: AccountValueStorage) {
self.storage = storage
public func removeUserInfo<Service: AccountService>(by accountService: Service) {
if let activeAccountService {
precondition(ObjectIdentifier(accountService) == ObjectIdentifier(activeAccountService)) // TODO message
}
if signedIn {
signedIn = false
}
user = nil
activeAccountService = nil
}
}
81 changes: 81 additions & 0 deletions Sources/SpeziAccount/AccountConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// This source file is part of the Spezi open-source project
//
// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import Foundation
import Spezi

// TODO can we split out some functionality into different targets? (e.g. AccountService side vs User side?

public protocol AccountServiceProvider: Component {
associatedtype Service: AccountService

var accountService: Service { get } // TODO one might provide multiple account services!
}

// TODO remove!
public class ExampleConfiguration<ComponentStandard: Standard>: AccountServiceProvider {
public var accountService: MockSimpleAccountService {
MockSimpleAccountService()
}
}

// TODO move somewhere else!
@resultBuilder
public enum AccountServiceProviderBuilder<S: Standard> {
public static func buildExpression<Provider: AccountServiceProvider>(
_ expression: @autoclosure @escaping () -> Provider
) -> [any ComponentDependency<S>] where Provider.ComponentStandard == S {
[_DependencyPropertyWrapper<Provider, S>(wrappedValue: expression())]
}

// TODO support conditionals etc!

public static func buildBlock(_ components: [any ComponentDependency<S>]...) -> [any ComponentDependency<S>] {
var result: [any ComponentDependency<S>] = []

for componentArray in components {
result.append(contentsOf: componentArray)
}

return result
}
}

public final class AccountConfiguration<ComponentStandard: Standard>: Component, ObservableObjectProvider {
@DynamicDependencies var dynamicDependencies: [any Component<ComponentStandard>]

public var observableObjects: [any ObservableObject] {
guard let account else {
fatalError("Tried to access ObservableObjectProvider before \(Self.self).configure() was called")
}

return [account]
}

private var account: Account?

public init() {
self._dynamicDependencies = DynamicDependencies(componentProperties: [])
}

public init(@AccountServiceProviderBuilder<ComponentStandard> _ components: @escaping () -> [any ComponentDependency<ComponentStandard>]) {
self._dynamicDependencies = DynamicDependencies(componentProperties: components())
}

public func configure() {
let accountServices: [any AccountService] = dynamicDependencies.map { dependency in
guard let serviceProvider = dependency as? any AccountServiceProvider else {
fatalError("Reached inconsistent state where dynamic dependency isn't a AccountServiceProvider: \(dependency)")
}

return serviceProvider.accountService
}

account = Account(services: accountServices)
}
}
14 changes: 5 additions & 9 deletions Sources/SpeziAccount/AccountService/AccountService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,6 @@

import SwiftUI

// TODO make everything public!

// TODO app needs access to the "primary?"/signed in (we don't support multi account sign ins!) account service!
// -> logout functionality
// -> AccountSummary
// -> allows for non-account-service-specific app implementations (e.g., easily switch for testing) => otherwise cast!

/// An Account Service is a set of components that is capable setting up and managing an ``Account`` context.
///
/// This base protocol imposes the minimal requirements for an AccountService where setup procedures are entirely
Expand All @@ -23,13 +16,13 @@ import SwiftUI
/// ``EmbeddableAccountService`` or ``KeyPasswordBasedAccountService``. TODO docs?
///
/// You can learn more about creating an account service at: <doc:CreateAnAccountService>.
public protocol AccountService { // TODO reevaluate DocC link!
public protocol AccountService: AnyObject { // TODO identifiable? do we wanna mandate Actor?
/// The ``AccountSetupViewStyle`` will be used to customized the look and feel of the ``AccountSetup`` view.
associatedtype ViewStyle: AccountSetupViewStyle

// TODO provide access to `Account` to communicate changes back to the App

var viewStyle: ViewStyle { get } // TODO this has to be a computed property as of right now!
var viewStyle: ViewStyle { get } // TODO document computed property!

/// This method implements ``Account`` logout functionality.
///
Expand All @@ -40,5 +33,8 @@ public protocol AccountService { // TODO reevaluate DocC link!
/// Make sure to remain in a state where the user is capable of retrying the logout process.
func logout() async throws

// TODO document requirement to store it as a weak reference!
func inject(account: Account)

// TODO we will/should enforce a Account removal functionality
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,21 @@ public struct UserIdPasswordServiceConfiguration {
public let image: Image

// TODO they are not the requirements? you might enter optional values, those are displayed but not required!
public let signUpRequirements: AccountValueRequirements // TODO replace this with a type that is queryable!
public let signUpRequirements: AccountValueRequirements

// TODO localization
public let userIdType: UserIdType
public let userIdField: FieldConfiguration

// TODO login and reset just validates non-empty!
// TODO a note on client side validation!
// TODO document: login and reset just validates non-empty! (=> a note on client side validation!)
public let userIdSignupValidations: [ValidationRule]
public let passwordSignupValidations: [ValidationRule]

public init(
name: LocalizedStringResource,
image: Image = defaultAccountImage,
signUpRequirements: AccountValueRequirements = AccountValueRequirements(), // TODO provide default!
signUpRequirements: AccountValueRequirements = .default,
userIdType: UserIdType = .emailAddress,
userIdField: FieldConfiguration = .username,
userIdField: FieldConfiguration = .emailAddress,
userIdSignupValidations: [ValidationRule] = [.nonEmpty],
passwordSignupValidations: [ValidationRule] = [.nonEmpty]
) {
Expand All @@ -63,7 +61,7 @@ public protocol UserIdPasswordAccountService: AccountService, EmbeddableAccountS

extension UserIdPasswordAccountService {
public var configuration: UserIdPasswordServiceConfiguration {
UserIdPasswordServiceConfiguration(name: "Default Account Service") // TODO how to pass this option?
UserIdPasswordServiceConfiguration(name: "Default Account Service") // TODO some sane defaults?
}
}

Expand Down
44 changes: 15 additions & 29 deletions Sources/SpeziAccount/AccountSetup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import AuthenticationServices
import SpeziViews
import SwiftUI

public enum Constants {
public enum Constants { // TODO rename!
static let outerHorizontalPadding: CGFloat = 16 // TODO use 32?
static let innerHorizontalPadding: CGFloat = 16 // TODO use 32?
static let maxFrameWidth: CGFloat = 450
}

/// A view which provides the default titlte and subtitlte text.
public struct DefaultHeader: View { // TODO rename!
/// A view which provides the default title and subtitle text.
public struct AccountSetupDefaultHeader: View { // TODO move
@EnvironmentObject
private var account: Account

Expand Down Expand Up @@ -63,7 +63,6 @@ public struct AccountSetup<Header: View>: View {
.filter { $0 is any EmbeddableAccountService }

if embeddableServices.count == 1 {
// TODO unwrap first first and then cast?
return embeddableServices.first as? any EmbeddableAccountService
}

Expand Down Expand Up @@ -92,13 +91,12 @@ public struct AccountSetup<Header: View>: View {

Spacer()

if let account = account.account {
if let account = account.user {
displayAccount(account: account)
} else {
noAccountState
}

// TODO provide ability to inject footer (e.g., terms and conditions?)
Spacer()
Spacer()
Spacer()
Expand Down Expand Up @@ -145,7 +143,6 @@ public struct AccountSetup<Header: View>: View {
@ViewBuilder var accountServicesSection: some View {
if let embeddableService = embeddableAccountService {
let embeddableViewStyle = embeddableService.viewStyle
// TODO i can get back type erasure right?
AnyView(embeddableViewStyle.makeEmbeddedAccountView())


Expand All @@ -158,7 +155,6 @@ public struct AccountSetup<Header: View>: View {
let service = nonEmbeddableAccountServices[index]
let style = service.viewStyle

// TODO embed into style?
NavigationLink {
AnyView(style.makePrimaryView())
} label: {
Expand All @@ -173,15 +169,10 @@ public struct AccountSetup<Header: View>: View {
let service = services[index]
let style = service.viewStyle

// TODO embed into style!
NavigationLink {
AnyView(style.makePrimaryView())
// TODO inject account service!! lol, nothing is typed!
} label: {
AnyView(style.makeAccountServiceButtonLabel())
// TODO inject account service!! lol, nothing is typed!

// TODO may we provide a default implementation, or work with a optional serviceButton style?
}
}
}
Expand Down Expand Up @@ -218,21 +209,16 @@ public struct AccountSetup<Header: View>: View {
.signInWithAppleButtonStyle(colorScheme == .light ? .black : .white)
}
}

// TODO we want to check if there is a single username/password provider and the rest are identity providers!
// the one need input fields! (and a secondary action!)
// the other need a single button
// => KeyPasswordBasedAuthentication[Service]
// => IdentityProvideBasedAuthentication[Service]
}

// TODO docs
public init(@ViewBuilder _ header: () -> Header = { DefaultHeader() }) {
public init(@ViewBuilder _ header: () -> Header = { AccountSetupDefaultHeader() }) {
self.header = header()
}

func displayAccount(account: AccountValuesWhat) -> some View {
let service = self.account.accountServices.first! // TODO how to get the primary account service!
func displayAccount(account: UserInfo) -> some View {
// TODO how to get the currently active account service!
let service = self.account.accountServices.first!

// TODO someone needs to place the Continue button?

Expand All @@ -245,18 +231,18 @@ struct AccountView_Previews: PreviewProvider {
static var accountServicePermutations: [[any AccountService]] = {
[
[DefaultUsernamePasswordAccountService()],
[RandomAccountService()],
[DefaultUsernamePasswordAccountService(), RandomAccountService()],
[MockSimpleAccountService()],
[DefaultUsernamePasswordAccountService(), MockSimpleAccountService()],
[
DefaultUsernamePasswordAccountService(),
RandomAccountService(),
MockSimpleAccountService(),
DefaultUsernamePasswordAccountService()
],
[]
]
}()

static let account1: AccountValuesWhat = AccountValueStorageBuilder()
static let account1: UserInfo = AccountValueStorageBuilder()
.add(UserIdAccountValueKey.self, value: "[email protected]")
.add(NameAccountValueKey.self, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer"))
.build()
Expand All @@ -266,15 +252,15 @@ struct AccountView_Previews: PreviewProvider {
NavigationStack {
AccountSetup()
}
.environmentObject(Account(accountServices: accountServicePermutations[index]))
.environmentObject(Account(services: accountServicePermutations[index]))
}

NavigationStack {
AccountSetup()
}
.environmentObject(Account(
accountServices: [DefaultUsernamePasswordAccountService()],
account: account1
account: account1,
active: DefaultUsernamePasswordAccountService()
))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ public protocol AccountSetupViewStyle {
associatedtype PrimaryView: View
associatedtype AccountSummaryView: View

// TODO that's not really a great way to deal with that?
var service: Service { get }

@ViewBuilder
Expand All @@ -27,5 +26,5 @@ public protocol AccountSetupViewStyle {
func makePrimaryView() -> PrimaryView

@ViewBuilder
func makeAccountSummary(account: AccountValuesWhat) -> AccountSummaryView
func makeAccountSummary(account: UserInfo) -> AccountSummaryView // TODO provide a default here!
}
Loading

0 comments on commit 4c84149

Please sign in to comment.