Skip to content

Commit

Permalink
Merge pull request #230 from stytchauth/member-client-parity
Browse files Browse the repository at this point in the history
[SDK-1474][B2B] Add methods for b2b member client
  • Loading branch information
nidal-stytch authored May 22, 2024
2 parents f7a309e + 35a943a commit 133396d
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 10 deletions.
2 changes: 2 additions & 0 deletions Sources/StytchCore/Environment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ struct Environment {

let userStorage: UserStorage = .init()

let memberStorage: MemberStorage = .init()

var localStorage: LocalStorage = .init()

var cookieClient: CookieClient = .live
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Generated using Sourcery 2.0.2 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
import Combine
import Foundation

public extension StytchB2BClient.Members {
/// Deletes, by id, an existing authentication factor associated with the current member.
func deleteFactor(_ factor: Member.AuthenticationFactor, completion: @escaping Completion<MemberResponse>) {
Task {
do {
completion(.success(try await deleteFactor(factor)))
} catch {
completion(.failure(error))
}
}
}

/// Deletes, by id, an existing authentication factor associated with the current member.
func deleteFactor(_ factor: Member.AuthenticationFactor) -> AnyPublisher<MemberResponse, Error> {
return Deferred {
Future({ promise in
Task {
do {
promise(.success(try await deleteFactor(factor)))
} catch {
promise(.failure(error))
}
}
})
}
.eraseToAnyPublisher()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Generated using Sourcery 2.0.2 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
import Combine
import Foundation

public extension StytchB2BClient.Members {
/// Updates the current member.
func update(parameters: UpdateParameters, completion: @escaping Completion<MemberResponse>) {
Task {
do {
completion(.success(try await update(parameters: parameters)))
} catch {
completion(.failure(error))
}
}
}

/// Updates the current member.
func update(parameters: UpdateParameters) -> AnyPublisher<MemberResponse, Error> {
return Deferred {
Future({ promise in
Task {
do {
promise(.success(try await update(parameters: parameters)))
} catch {
promise(.failure(error))
}
}
})
}
.eraseToAnyPublisher()
}
}
28 changes: 28 additions & 0 deletions Sources/StytchCore/MemberStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Combine
import Foundation

final class MemberStorage {
private(set) var member: Member? {
get { localStorage.member }
set { localStorage.member = newValue }
}

private(set) lazy var onMemberChange = _onMemberChange
.map { [weak self] in self?.member == nil }
.removeDuplicates()
.map { [weak self] _ in self?.member }

private let _onMemberChange = PassthroughSubject<Void, Never>()

@Dependency(\.localStorage) private var localStorage

func updateMember(_ member: Member) {
self.member = member
_onMemberChange.send(())
}

func reset() {
member = nil
_onMemberChange.send(())
}
}
8 changes: 8 additions & 0 deletions Sources/StytchCore/StytchB2BClient/Models/Member.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public struct Member: Codable {
public let ssoRegistrations: [SSORegistration]
public let trustedMetadata: JSON
public let untrustedMetadata: JSON
public let memberPasswordId: String

let memberId: ID
}
Expand All @@ -29,6 +30,13 @@ public extension Member {
self = .init(rawValue: rawValue) ?? .unknown
}
}

/// The authentication factors which are able to be managed via member-management calls.
enum AuthenticationFactor {
case totp
case phoneNumber
case password(passwordId: String)
}
}

/// A type representing a specific SSO registration.
Expand Down
79 changes: 74 additions & 5 deletions Sources/StytchCore/StytchB2BClient/StytchB2BClient+Members.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
import Combine

public extension StytchB2BClient {
/// The interface for interacting with member products.
static var member: Members { .init(router: organization.router.scopedRouter { $0.members }) }
}

public extension StytchB2BClient {
/// The SDK allows you to view the current member's information, such as fetching (or viewing the most recent cached version) of the current member.
struct Members {
let router: NetworkingRouter<StytchB2BClient.OrganizationsRoute.MembersRoute>

@Dependency(\.localStorage) private var localStorage
@Dependency(\.memberStorage) private var memberStorage

/// A publisher which emits following a change in member status and returns either the member object or nil. You can use this as an indicator to set up or tear down your UI accordingly.
public var onMemberChange: AnyPublisher<Member?, Never> {
memberStorage.onMemberChange.eraseToAnyPublisher()
}

/// Returns the most-recent cached copy of the member object, if it has already been fetched via another method, else nil.
public func getSync() -> Member? {
Expand All @@ -17,12 +30,37 @@ public extension StytchB2BClient {
localStorage.member = response.member
return response
}
}
}

public extension StytchB2BClient {
/// The interface for interacting with member products.
static var member: Members { .init(router: organization.router.scopedRouter { $0.members }) }
// sourcery: AsyncVariants
/// Updates the current member.
public func update(parameters: UpdateParameters) async throws -> MemberResponse {
try await updatingCachedMember {
try await router.put(to: .update, parameters: parameters)
}
}

// sourcery: AsyncVariants
/// Deletes, by id, an existing authentication factor associated with the current member.
public func deleteFactor(_ factor: Member.AuthenticationFactor) async throws -> MemberResponse {
let response: MemberResponse
switch factor {
case .totp:
response = try await router.delete(route: .deleteTOTP)
case .phoneNumber:
response = try await router.delete(route: .deletePhoneNumber)
case let .password(passwordId):
response = try await router.delete(route: .deletePassword(passwordId: passwordId))
}
memberStorage.updateMember(response.member)
return response
}

private func updatingCachedMember(_ performRequest: () async throws -> MemberResponse) async rethrows -> MemberResponse {
let response = try await performRequest()
memberStorage.updateMember(response.wrapped.member)
return response
}
}
}

public extension StytchB2BClient.Members {
Expand All @@ -31,7 +69,38 @@ public extension StytchB2BClient.Members {

/// The underlying data for the MemberResponse type.
struct MemberResponseData: Codable {
/// The current member's member id.
public let memberId: String?

/// The current member.
public let member: Member

/// The current member's organization.
public let organization: Organization?
}
}

public extension StytchB2BClient.Members {
/// The dedicated parameters type for the update member call.
struct UpdateParameters: Codable {
let name: String?
let untrustedMetadata: JSON?
let mfaEnrolled: Bool?
let mfaPhoneNumber: String?
let defaultMfaMethod: String?

public init(
name: String? = nil,
untrustedMetadata: JSON? = nil,
mfaEnrolled: Bool? = nil,
mfaPhoneNumber: String? = nil,
defaultMfaMethod: String? = nil
) {
self.name = name
self.untrustedMetadata = untrustedMetadata
self.mfaEnrolled = mfaEnrolled
self.mfaPhoneNumber = mfaPhoneNumber
self.defaultMfaMethod = defaultMfaMethod
}
}
}
19 changes: 17 additions & 2 deletions Sources/StytchCore/StytchB2BClient/StytchB2BClient+Routes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,27 @@ extension StytchB2BClient {
}
}

enum MembersRoute: String, RouteType {
enum MembersRoute: RouteType {
// swiftlint:disable:next identifier_name
case me
case update
case deletePhoneNumber
case deleteTOTP
case deletePassword(passwordId: String)

var path: Path {
.init(rawValue: rawValue)
switch self {
case .me:
return Path(rawValue: "me")
case .update:
return Path(rawValue: "update")
case .deletePhoneNumber:
return Path(rawValue: "deletePhoneNumber")
case .deleteTOTP:
return Path(rawValue: "deleteTOTP")
case let .deletePassword(passwordId):
return Path(rawValue: "passwords/\(passwordId)")
}
}
}
}
Expand Down
Loading

0 comments on commit 133396d

Please sign in to comment.