Skip to content

Commit

Permalink
Initial draft of the AccountValue system; some localization, Account …
Browse files Browse the repository at this point in the history
…Summary Views
  • Loading branch information
Supereg committed Jul 2, 2023
1 parent 4e6a81e commit af9636a
Show file tree
Hide file tree
Showing 35 changed files with 794 additions and 238 deletions.
21 changes: 20 additions & 1 deletion Sources/SpeziAccount/Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@ import SwiftUI


/// Account-related Spezi module managing a collection of ``AccountService``s.
/// TODO update docs!
///
/// The ``Account/Account`` type also enables interaction with the ``AccountService``s from anywhere in the view hierarchy.
public actor 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 var signedIn = false
public private(set) var account: AccountValuesWhat? // TODO UserAccount/User! TODO must only be accessible/modifieable through an AccountService!

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

Expand All @@ -34,4 +40,17 @@ public actor Account: ObservableObject {
// accountService.inject(account: self)
// }
}

init(accountServices: [any AccountService] = [], account: AccountValuesWhat) {
self.accountServices = accountServices
self._account = Published(wrappedValue: account)
}
}

public struct AccountValuesWhat: Sendable, ModifiableAccountValueStorageContainer { // TODO naming is off!
public var storage: AccountValueStorage

public init(storage: AccountValueStorage) {
self.storage = storage
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import Foundation
import SwiftUI

// TODO this configuration should be user accessible (userIdType, userIdField config)!
public struct UserIdPasswordServiceConfiguration {
public static var defaultAccountImage: Image {
Image(systemName: "person.crop.circle.fill")
Expand All @@ -19,9 +20,11 @@ public struct UserIdPasswordServiceConfiguration {
public let name: LocalizedStringResource
public let image: Image

public let signUpOptions: SignUpOptions
// 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!

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

// TODO login and reset just validates non-empty!
Expand All @@ -32,14 +35,16 @@ public struct UserIdPasswordServiceConfiguration {
public init(
name: LocalizedStringResource,
image: Image = defaultAccountImage,
signUpOptions: SignUpOptions = .default,
signUpRequirements: AccountValueRequirements = AccountValueRequirements(), // TODO provide default!
userIdType: UserIdType = .emailAddress,
userIdField: FieldConfiguration = .username,
userIdSignupValidations: [ValidationRule] = [.nonEmpty],
passwordSignupValidations: [ValidationRule] = [.nonEmpty]
) {
self.name = name
self.image = image
self.signUpOptions = signUpOptions
self.signUpRequirements = signUpRequirements
self.userIdType = userIdType
self.userIdField = userIdField
self.userIdSignupValidations = userIdSignupValidations
self.passwordSignupValidations = passwordSignupValidations
Expand All @@ -51,8 +56,7 @@ public protocol UserIdPasswordAccountService: AccountService, EmbeddableAccountS

func login(userId: String, password: String) async throws

// TODO ability to abstract SignUpValues
func signUp(signUpValues: SignUpValues) async throws // TODO refactor SignUpValues property names!
func signUp(signupRequest: SignupRequest) async throws

func resetPassword(userId: String) async throws
}
Expand Down
182 changes: 124 additions & 58 deletions Sources/SpeziAccount/AccountSetup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,58 @@
//

import AuthenticationServices
import SpeziViews
import SwiftUI

struct AccountSetup: View {
public enum Constants {
static let outerHorizontalPadding: CGFloat = 16 // TODO use 32?
static let innerHorizontalPadding: CGFloat = 16 // TODO use 32?
static let maxFrameWidth: CGFloat = 450
}
public enum Constants {
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!
@EnvironmentObject
var account: Account
private var account: Account

public var body: some View {
// TODO provide customizable with AccountViewStyle!
Text("ACCOUNT_WELCOME".localized(.module))
.font(.largeTitle)
.bold()
.multilineTextAlignment(.center)
.padding(.bottom)
.padding(.top, 30)

Group {
if !account.signedIn {
Text("ACCOUNT_WELCOME_SUBTITLE".localized(.module))
} else {
Text("ACCOUNT_WELCOME_SIGNED_IN_SUBTITLE".localized(.module))
}
}
.multilineTextAlignment(.center)
}

public init() {}
}

public struct AccountSetup<Header: View>: View {
private let header: Header

@EnvironmentObject var account: Account
@Environment(\.colorScheme)
var colorScheme

var services: [any AccountService] {
private var services: [any AccountService] {
account.accountServices
}

var embeddableAccountService: (any EmbeddableAccountService)? {
private var identityProviders: [String] {
["Apple"] // TODO query from account and model them
}

private var embeddableAccountService: (any EmbeddableAccountService)? {
let embeddableServices = services
.filter { $0 is any EmbeddableAccountService }

Expand All @@ -35,35 +70,35 @@ struct AccountSetup: View {
return nil
}

var nonEmbeddableAccountServices: [any AccountService] {
private var nonEmbeddableAccountServices: [any AccountService] {
services
.filter { !($0 is any EmbeddableAccountService) }
}

@Environment(\.colorScheme)
var colorScheme
private var documentationUrl: URL {
// we may move to a #URL macro once Swift 5.9 is shipping
guard let docsUrl = URL(string: "https://swiftpackageindex.com/stanfordspezi/speziaccount/documentation/speziaccount/createanaccountservice") else {
fatalError("Failed to construct SpeziAccount Documentation URL. Please review URL syntax!")
}

return docsUrl
}

var body: some View {
public var body: some View {
GeometryReader { proxy in
ScrollView(.vertical) {
VStack {
// TODO draw account summary if we are already signed in!
header

Spacer()

VStack {
primaryAccountServicesReplacement

// TODO show divider only if there is a least one account service AND identity provider!
servicesDivider

identityProviderButtons
if let account = account.account {
displayAccount(account: account)
} else {
noAccountState
}
.padding(.horizontal, Constants.innerHorizontalPadding)
.frame(maxWidth: Constants.maxFrameWidth) // landscape optimizations
// TODO for large dynamic size it would make sense to scale it though?

// TODO provide ability to inject footer (e.g., terms and conditions?)
Spacer()
Spacer()
Spacer()
Expand All @@ -75,30 +110,43 @@ struct AccountSetup: View {
}
}

/// The Views Title and subtitle text.
@ViewBuilder
var header: some View {
// TODO provide customizable with AccountViewStyle!
Text("Welcome! 👋") // TODO localize
.font(.largeTitle)
.bold()
.multilineTextAlignment(.center)
.padding(.bottom)
.padding(.top, 30)
@ViewBuilder var noAccountState: some View {
if services.isEmpty && identityProviders.isEmpty {
showEmptyView
} else {
VStack {
accountServicesSection

Text("Please create an account to do whatever. You may create an account if you don't have one already!") // TODO localize!
if !services.isEmpty && !identityProviders.isEmpty {
servicesDivider
}

identityProviderSection
}
.padding(.horizontal, Constants.innerHorizontalPadding)
.frame(maxWidth: Constants.maxFrameWidth) // landscape optimizations
// TODO for large dynamic size it would make sense to scale it though?
}
}

@ViewBuilder var showEmptyView: some View {
Text("MISSING_ACCOUNT_SERVICES".localized(.module))
.multilineTextAlignment(.center)
.foregroundColor(.secondary)

Button(action: {
UIApplication.shared.open(documentationUrl)
}) {
Text("OPEN_DOCUMENTATION".localized(.module))
}
.padding()
}

@ViewBuilder
var primaryAccountServicesReplacement: some View {
if services.isEmpty {
Text("Empty!! :(((") // TODO only place hint if there are not even identity providers!
} else if let embeddableService = embeddableAccountService {
@ViewBuilder var accountServicesSection: some View {
if let embeddableService = embeddableAccountService {
let embeddableViewStyle = embeddableService.viewStyle
// TODO i can get back type erasure right?
AnyView(embeddableViewStyle.makeEmbeddedAccountView())
// TODO inject account service!! lol, nothing is typed!


if !nonEmbeddableAccountServices.isEmpty {
Expand Down Expand Up @@ -135,20 +183,12 @@ struct AccountSetup: View {

// TODO may we provide a default implementation, or work with a optional serviceButton style?
}
/*
Button(action: {
print("Navigation?")
}) {
Text("Account service \(index)") // TODO we need a name?
}
*/
}
}
}

// The "or" divider between primary account services and the third-party identity providers
@ViewBuilder
var servicesDivider: some View {
@ViewBuilder var servicesDivider: some View {
HStack {
VStack {
Divider()
Expand All @@ -166,17 +206,17 @@ struct AccountSetup: View {
}

/// VStack of buttons provided by the identity providers
@ViewBuilder
var identityProviderButtons: some View {
@ViewBuilder var identityProviderSection: some View {
VStack {
SignInWithAppleButton { request in
print("Sign in request!")
} onCompletion: { result in
print("sing in completed")
}
ForEach(identityProviders.indices, id: \.self) { index in
SignInWithAppleButton { request in
print("Sign in request!")
} onCompletion: { result in
print("sing in completed")
}
.frame(height: 55)

.signInWithAppleButtonStyle(colorScheme == .light ? .black : .white)
}
}

// TODO we want to check if there is a single username/password provider and the rest are identity providers!
Expand All @@ -185,6 +225,19 @@ struct AccountSetup: View {
// => KeyPasswordBasedAuthentication[Service]
// => IdentityProvideBasedAuthentication[Service]
}

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

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

// TODO someone needs to place the Continue button?

return AnyView(service.viewStyle.makeAccountSummary(account: account))
}
}

#if DEBUG
Expand All @@ -203,13 +256,26 @@ struct AccountView_Previews: PreviewProvider {
]
}()

static let account1: AccountValuesWhat = AccountValueStorageBuilder()
.add(UserIdAccountValueKey.self, value: "[email protected]")
.add(NameAccountValueKey.self, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer"))
.build()

static var previews: some View {
ForEach(accountServicePermutations.indices, id: \.self) { index in
NavigationStack {
AccountSetup()
}
.environmentObject(Account(accountServices: accountServicePermutations[index]))
}

NavigationStack {
AccountSetup()
}
.environmentObject(Account(
accountServices: [DefaultUsernamePasswordAccountService()],
account: account1
))
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ public protocol AccountSetupViewStyle {
func makePrimaryView() -> PrimaryView

@ViewBuilder
func makeAccountSummary() -> AccountSummaryView
func makeAccountSummary(account: AccountValuesWhat) -> AccountSummaryView
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ extension UserIdPasswordAccountSetupViewStyle {
}
}

public func makeAccountSummary() -> some View {
DefaultUserIdPasswordAccountSummaryView(using: service)
public func makeAccountSummary(account: AccountValuesWhat) -> some View {
DefaultUserIdPasswordAccountSummaryView(using: service, account: account)
}

public func makeAccountServiceButtonLabel() -> some View {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ struct DefaultUsernamePasswordAccountService: UserIdPasswordAccountService {
try? await Task.sleep(nanoseconds: 1000_000_000)
}

func signUp(signUpValues: SignUpValues) async throws {
print("signup \(signUpValues)")
func signUp(signupRequest: SignupRequest) async throws {
print("signup \(signupRequest)")
try? await Task.sleep(nanoseconds: 1000_000_000)
}

Expand All @@ -36,5 +36,6 @@ struct DefaultUsernamePasswordAccountService: UserIdPasswordAccountService {

func logout() async throws {
print("logout")
try? await Task.sleep(nanoseconds: 1000_000_000)
}
}
Loading

0 comments on commit af9636a

Please sign in to comment.