diff --git a/.run/newm-mobile_shared [assembleXCFramework].run.xml b/.run/newm-mobile_shared [assembleXCFramework].run.xml new file mode 100644 index 00000000..f5b4bfec --- /dev/null +++ b/.run/newm-mobile_shared [assembleXCFramework].run.xml @@ -0,0 +1,28 @@ + + + + + + + + true + true + false + + + \ No newline at end of file diff --git a/android/app-newm/src/main/java/io/newm/di/android/Dependencies.kt b/android/app-newm/src/main/java/io/newm/di/android/Dependencies.kt index 362ef669..8b088fde 100644 --- a/android/app-newm/src/main/java/io/newm/di/android/Dependencies.kt +++ b/android/app-newm/src/main/java/io/newm/di/android/Dependencies.kt @@ -55,7 +55,7 @@ val viewModule = module { WelcomeScreenPresenter( navigator = params.get(), googleSignInLauncher = get(), - repository = get(), + loginUseCase = get(), activityResultContract = ActivityResultContracts.StartActivityForResult() ) } diff --git a/android/features/login/src/main/java/io/newm/feature/login/screen/welcome/WelcomeScreenPresenter.kt b/android/features/login/src/main/java/io/newm/feature/login/screen/welcome/WelcomeScreenPresenter.kt index efa9fe14..6ee6eaba 100644 --- a/android/features/login/src/main/java/io/newm/feature/login/screen/welcome/WelcomeScreenPresenter.kt +++ b/android/features/login/src/main/java/io/newm/feature/login/screen/welcome/WelcomeScreenPresenter.kt @@ -1,7 +1,6 @@ package io.newm.feature.login.screen.welcome import android.content.Intent -import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContract @@ -22,12 +21,12 @@ import io.newm.feature.login.screen.LoginScreen import io.newm.feature.login.screen.authproviders.google.GoogleSignInLauncher import io.newm.feature.login.screen.createaccount.CreateAccountScreen import io.newm.shared.login.repository.KMMException -import io.newm.shared.login.repository.LogInRepository import io.newm.shared.login.repository.OAuthData +import io.newm.shared.usecases.LoginUseCase class WelcomeScreenPresenter( private val navigator: Navigator, - private val repository: LogInRepository, + private val loginUseCase: LoginUseCase, private val googleSignInLauncher: GoogleSignInLauncher, private val activityResultContract: ActivityResultContract, ) : Presenter { @@ -56,7 +55,7 @@ class WelcomeScreenPresenter( val idToken = account.idToken idToken ?: throw IllegalStateException("Google sign in failed. idToken is null") - repository.oAuthLogin(OAuthData.Google(idToken)) + loginUseCase.logInWithGoogle(idToken) navigator.goTo(HomeScreen) } catch (e: ApiException) { // The ApiException status code indicates the detailed failure reason. diff --git a/gradle.properties b/gradle.properties index 6d37e097..08f9aa8d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,4 +17,4 @@ android.useAndroidX=true #MPP kotlin.mpp.enableCInteropCommonization=true kotlin.native.binary.memoryModel=experimental -kotlin.mpp.androidSourceSetLayoutVersion=2 \ No newline at end of file +kotlin.mpp.androidSourceSetLayoutVersion=2 diff --git a/iosApp/Modules/DI/ModuleLinker/Extensions/Error+KMMException.swift b/iosApp/Modules/DI/ModuleLinker/Extensions/Error+KMMException.swift new file mode 100644 index 00000000..f8228ab4 --- /dev/null +++ b/iosApp/Modules/DI/ModuleLinker/Extensions/Error+KMMException.swift @@ -0,0 +1,6 @@ +import Foundation +import shared + +public extension Error { + var kmmException: KMMException? { (self as NSError).kotlinException as? KMMException } +} diff --git a/iosApp/Modules/DI/ModuleLinker/Extensions/Notification+KMM.swift b/iosApp/Modules/DI/ModuleLinker/Extensions/Notification+KMM.swift new file mode 100644 index 00000000..90d679e5 --- /dev/null +++ b/iosApp/Modules/DI/ModuleLinker/Extensions/Notification+KMM.swift @@ -0,0 +1,7 @@ +import Foundation + +public extension NotificationCenter { + func publisher(for name: String) -> Publisher { + publisher(for: Notification.Name(name), object: nil) + } +} diff --git a/iosApp/Modules/DI/ModuleLinker/Protocols/Data.swift b/iosApp/Modules/DI/ModuleLinker/Protocols/Data.swift index 2b2e2945..1acfa233 100644 --- a/iosApp/Modules/DI/ModuleLinker/Protocols/Data.swift +++ b/iosApp/Modules/DI/ModuleLinker/Protocols/Data.swift @@ -1,19 +1,19 @@ -import Foundation -import Models - -public protocol UserManaging: ObservableObject { - var currentUser: User? { get } - - func deleteUser() async throws - func createUser(nickname: String, email: String, password: String, passwordConfirmation: String, verificationCode: String) async throws - func resetPassword(email: String, password: String, confirmPassword: String, authCode: String) async throws - func fetchCurrentUser() async throws - func updateUserInfo(firstName: String?, lastName: String?, currentPassword: String?, newPassword: String?, confirmNewPassword: String?) async throws -} - -public enum UserRepoError: LocalizedError { - case accountExists - case twoFAFailed - case dataUnavailable - case displayError(String) -} +//import Foundation +//import Models +// +//public protocol UserManaging: ObservableObject { +// var currentUser: User? { get } +// +// func deleteUser() async throws +// func createUser(nickname: String, email: String, password: String, passwordConfirmation: String, verificationCode: String) async throws +// func resetPassword(email: String, password: String, confirmPassword: String, authCode: String) async throws +// func fetchCurrentUser() async throws +// func updateUserInfo(firstName: String?, lastName: String?, currentPassword: String?, newPassword: String?, confirmNewPassword: String?) async throws +//} +// +//public enum UserRepoError: LocalizedError { +// case accountExists +// case twoFAFailed +// case dataUnavailable +// case displayError(String) +//} diff --git a/iosApp/Modules/Data/DataModule.swift b/iosApp/Modules/Data/DataModule.swift index f22be5ec..eb4cf306 100644 --- a/iosApp/Modules/Data/DataModule.swift +++ b/iosApp/Modules/Data/DataModule.swift @@ -7,9 +7,6 @@ public final class DataModule: ModuleProtocol { public static let shared = DataModule() public func registerAllServices() { - Resolver.register { - UserRepo.shared as any UserManaging - } } } diff --git a/iosApp/Modules/Data/Login/LoginRepo.swift b/iosApp/Modules/Data/Login/LoginRepo.swift index a3375a84..a4db4b06 100644 --- a/iosApp/Modules/Data/Login/LoginRepo.swift +++ b/iosApp/Modules/Data/Login/LoginRepo.swift @@ -1,70 +1,67 @@ -import Foundation -import API -import KeychainSwift -import FacebookLogin -import GoogleSignIn -import AuthenticationServices - -enum LoginError: Error { - case facebookAccessTokenMissing -} - -public class LoginRepo: ObservableObject { - //TODO: update this with user info - @Published public var userIsLoggedIn: Bool = false - private let api: LoginAPI - - private var appleSignInID: String? - - //TODO: make actor - public static let shared = LoginRepo() - - private init(api: LoginAPI = LoginAPI()) { - self.api = api - api.$userIsLoggedIn.assign(to: &$userIsLoggedIn) - } - - public func loginWithFacebook(result: LoginManagerLoginResult) async throws { - do { - guard let token = result.token?.tokenString else { - throw LoginError.facebookAccessTokenMissing - } - try await api.login(method: .facebook(accessToken: token)) - } catch { - Task { @MainActor in - FacebookLogin.LoginManager().logOut() - } - throw error - } - } - - public func loginWithGoogle(result: GIDSignInResult) async throws { - let token = result.user.accessToken.tokenString - try await api.login(method: .google(accessToken: token)) - } - - public func login(email: String, password: String) async throws { - try await api.login(method: .email(email: email, password: password)) - } - - public func logOut() { - api.logOut() - FacebookLogin.LoginManager().logOut() - GIDSignIn.sharedInstance.signOut() - signOutOfApple() - UserRepo.shared.currentUser = nil - } - - public func requestEmailVerificationCode(for email: String) async throws { - try await api.requestEmailVerificationCode(for: email) - } - - public func loginWithApple(result: ASAuthorization) async throws { - let token = String(data: (result.credential as! ASAuthorizationAppleIDCredential).identityToken!, encoding: .utf8)! - try await api.login(method: .apple(idToken: token)) - } - - private func signOutOfApple() { - //TODO: - } -} +//import Foundation +//import API +//import KeychainSwift +//import FacebookLogin +//import GoogleSignIn +//import AuthenticationServices +// +//enum LoginError: Error { +// case facebookAccessTokenMissing +//} +// +//public class LoginRepo: ObservableObject { +// //TODO: update this with user info +// @Published public var userIsLoggedIn: Bool = false +// private let api: LoginAPI +// +// private var appleSignInID: String? +// +// private init(api: LoginAPI = LoginAPI()) { +// self.api = api +// api.$userIsLoggedIn.assign(to: &$userIsLoggedIn) +// } +// +// public func loginWithFacebook(result: LoginManagerLoginResult) async throws { +// do { +// guard let token = result.token?.tokenString else { +// throw LoginError.facebookAccessTokenMissing +// } +// try await api.login(method: .facebook(accessToken: token)) +// } catch { +// Task { @MainActor in +// FacebookLogin.LoginManager().logOut() +// } +// throw error +// } +// } +// +// public func loginWithGoogle(result: GIDSignInResult) async throws { +// let token = result.user.accessToken.tokenString +// try await api.login(method: .google(accessToken: token)) +// } +// +// public func login(email: String, password: String) async throws { +// try await api.login(method: .email(email: email, password: password)) +// } +// +// public func logOut() { +// api.logOut() +// FacebookLogin.LoginManager().logOut() +// GIDSignIn.sharedInstance.signOut() +// signOutOfApple() +// UserRepo.shared.currentUser = nil +// } +// +// public func requestEmailVerificationCode(for email: String) async throws { +// try await api.requestEmailVerificationCode(for: email) +// } +// +// public func loginWithApple(result: ASAuthorization) async throws { +// let token = String(data: (result.credential as! ASAuthorizationAppleIDCredential).identityToken!, encoding: .utf8)! +// try await api.login(method: .apple(idToken: token)) +// } +// +// private func signOutOfApple() { +// //TODO: +// } +//} diff --git a/iosApp/Modules/Data/User/UserRepo.swift b/iosApp/Modules/Data/User/UserRepo.swift index a726c0fb..16490e5a 100644 --- a/iosApp/Modules/Data/User/UserRepo.swift +++ b/iosApp/Modules/Data/User/UserRepo.swift @@ -1,84 +1,86 @@ -import Foundation -import API -import Models -import ModuleLinker - -class UserRepo: UserManaging, ObservableObject { - private let api = UserAPI() - - @Published public var currentUser: User? - - //TODO: make actor - public static let shared = UserRepo() - - private init() {} - - public func deleteUser() async throws { - do { - try await api.delete() - } catch { - try handleError(error) - } - LoginRepo.shared.logOut() - } - - public func createUser(nickname: String, email: String, password: String, passwordConfirmation: String, verificationCode: String) async throws { - do { - try await api.create(nickname: nickname, email: email, password: password, passwordConfirmation: passwordConfirmation, verificationCode: verificationCode) - } catch { - try handleError(error) - } - } - - public func resetPassword(email: String, password: String, confirmPassword: String, authCode: String) async throws { - do { - try await api.resetPassword(email: email, password: password, confirmPassword: confirmPassword, authCode: authCode) - } catch { - try handleError(error) - } - } - - public func fetchCurrentUser() async throws { - do { - currentUser = try await api.getCurrentUser() - } catch { - try handleError(error) - } - } - - public func updateUserInfo(firstName: String?, lastName: String?, currentPassword: String?, newPassword: String?, confirmNewPassword: String?) async throws { - let updatedUser = try UpdatedUser( - firstName: firstName?.nilIfEmpty, - lastName: lastName?.nilIfEmpty, - newPassword: newPassword?.nilIfEmpty, - confirmPassword: confirmNewPassword?.nilIfEmpty, - currentPassword: currentPassword?.nilIfEmpty - ) - do { - try await api.update(user: updatedUser) - try await fetchCurrentUser() - } catch { - try handleError(error) - } - } - - private func handleError(_ error: Error) throws { - if let error = error as? UserAPI.Error { - switch error { - case .accountExists: - throw UserRepoError.accountExists - case .twoFAFailed: - throw UserRepoError.twoFAFailed - case .unprocessableEntity(let cause): - throw UserRepoError.displayError(cause) - } - } else if let error = error as? APIError { - switch error { - case .invalidResponse: - throw UserRepoError.dataUnavailable - case .httpError(_, let cause): - throw UserRepoError.displayError(cause ?? "An unknown error occurred.") - } - } - } -} +//import Foundation +//import API +//import Models +//import ModuleLinker +//import Resolver +// +//class UserRepo: UserManaging, ObservableObject { +// private let api = UserAPI() +// @Injected private var loginRepo: LoginRepo +// +// @Published public var currentUser: User? +// +// //TODO: make actor +// public static let shared = UserRepo() +// +// private init() {} +// +// public func deleteUser() async throws { +// do { +// try await api.delete() +// } catch { +// try handleError(error) +// } +// loginRepo.logOut() +// } +// +// public func createUser(nickname: String, email: String, password: String, passwordConfirmation: String, verificationCode: String) async throws { +// do { +// try await api.create(nickname: nickname, email: email, password: password, passwordConfirmation: passwordConfirmation, verificationCode: verificationCode) +// } catch { +// try handleError(error) +// } +// } +// +// public func resetPassword(email: String, password: String, confirmPassword: String, authCode: String) async throws { +// do { +// try await api.resetPassword(email: email, password: password, confirmPassword: confirmPassword, authCode: authCode) +// } catch { +// try handleError(error) +// } +// } +// +// public func fetchCurrentUser() async throws { +// do { +// currentUser = try await api.getCurrentUser() +// } catch { +// try handleError(error) +// } +// } +// +// public func updateUserInfo(firstName: String?, lastName: String?, currentPassword: String?, newPassword: String?, confirmNewPassword: String?) async throws { +// let updatedUser = try UpdatedUser( +// firstName: firstName?.nilIfEmpty, +// lastName: lastName?.nilIfEmpty, +// newPassword: newPassword?.nilIfEmpty, +// confirmPassword: confirmNewPassword?.nilIfEmpty, +// currentPassword: currentPassword?.nilIfEmpty +// ) +// do { +// try await api.update(user: updatedUser) +// try await fetchCurrentUser() +// } catch { +// try handleError(error) +// } +// } +// +// private func handleError(_ error: Error) throws { +// if let error = error as? UserAPI.Error { +// switch error { +// case .accountExists: +// throw UserRepoError.accountExists +// case .twoFAFailed: +// throw UserRepoError.twoFAFailed +// case .unprocessableEntity(let cause): +// throw UserRepoError.displayError(cause) +// } +// } else if let error = error as? APIError { +// switch error { +// case .invalidResponse: +// throw UserRepoError.dataUnavailable +// case .httpError(_, let cause): +// throw UserRepoError.displayError(cause ?? "An unknown error occurred.") +// } +// } +// } +//} diff --git a/iosApp/Modules/Helpers/SharedUI/SwiftUI+Common.swift b/iosApp/Modules/Helpers/SharedUI/SwiftUI+Common.swift index 7ce42d12..b773b912 100644 --- a/iosApp/Modules/Helpers/SharedUI/SwiftUI+Common.swift +++ b/iosApp/Modules/Helpers/SharedUI/SwiftUI+Common.swift @@ -88,8 +88,8 @@ public struct BorderOverlay: ViewModifier { public func body(content: Content) -> some View { content - .overlay(RoundedRectangle(cornerRadius: radius) - .stroke(color, lineWidth: width)) +// .overlay(RoundedRectangle(cornerRadius: radius) +// .stroke(color, lineWidth: width)) .erased } } diff --git a/iosApp/Modules/Screens/Home/HomeModule.swift b/iosApp/Modules/Screens/Home/HomeModule.swift index 2f5ca5db..e0f07cf3 100644 --- a/iosApp/Modules/Screens/Home/HomeModule.swift +++ b/iosApp/Modules/Screens/Home/HomeModule.swift @@ -19,10 +19,6 @@ extension HomeModule { mockResolver.register { MockHomeViewUIModelProvider(actionHandler: $0.resolve()) as HomeViewUIModelProviding } - - mockResolver.register { - MockUserRepo() as any UserManaging - } } } #endif diff --git a/iosApp/Modules/Screens/Home/Mocks/MockUserManager.swift b/iosApp/Modules/Screens/Home/Mocks/MockUserManager.swift index 02899f68..ca156911 100644 --- a/iosApp/Modules/Screens/Home/Mocks/MockUserManager.swift +++ b/iosApp/Modules/Screens/Home/Mocks/MockUserManager.swift @@ -1,60 +1,60 @@ -import Foundation -import Data -import Models -import ModuleLinker - -class MockUserRepo: UserManaging { - func deleteUser() async throws { - - } - - func createUser(nickname: String, email: String, password: String, passwordConfirmation: String, verificationCode: String) async throws { - - } - - func resetPassword(email: String, password: String, confirmPassword: String, authCode: String) async throws { - - } - - func fetchCurrentUser() async throws { - currentUser = User( - id: "1", - createdAt: "today", - firstName: "Joe", - lastName: "Blow", - nickname: "Joe Blow", - pictureUrl: URL(string: "https://resizing.flixster.com/xhyRkgdbTuATF4u0C2pFWZQZZtw=/300x300/v2/https://flxt.tmsimg.com/assets/p175884_k_v9_ae.jpg")!, - bannerUrl: URL(string: "https://www.sonypictures.com/sites/default/files/styles/max_360x390/public/banner-images/2020-04/stepbrothers_banner_2572x1100_v2.png?h=abc6acbe&itok=7m7ZKbdz")!, - email: "joe@blow.com" - ) - } - - func updateUserInfo(firstName: String?, lastName: String?, currentPassword: String?, newPassword: String?, confirmNewPassword: String?) async throws { - firstName.flatMap { - guard let currentUser else { return } - self.currentUser = User(id: currentUser.id, - createdAt: currentUser.createdAt, - firstName: $0, - lastName: currentUser.lastName, - nickname: currentUser.nickname, - pictureUrl: currentUser.pictureUrl, - bannerUrl: currentUser.bannerUrl, - email: currentUser.email) - } - lastName.flatMap { - guard let currentUser else { return } - self.currentUser = User(id: currentUser.id, - createdAt: currentUser.createdAt, - firstName: currentUser.firstName, - lastName: $0, - nickname: currentUser.nickname, - pictureUrl: currentUser.pictureUrl, - bannerUrl: currentUser.bannerUrl, - email: currentUser.email) - } - } - - init() {} - - var currentUser: User? -} +//import Foundation +//import Data +//import Models +//import ModuleLinker +// +//class MockUserRepo: UserManaging { +// func deleteUser() async throws { +// +// } +// +// func createUser(nickname: String, email: String, password: String, passwordConfirmation: String, verificationCode: String) async throws { +// +// } +// +// func resetPassword(email: String, password: String, confirmPassword: String, authCode: String) async throws { +// +// } +// +// func fetchCurrentUser() async throws { +// currentUser = User( +// id: "1", +// createdAt: "today", +// firstName: "Joe", +// lastName: "Blow", +// nickname: "Joe Blow", +// pictureUrl: URL(string: "https://resizing.flixster.com/xhyRkgdbTuATF4u0C2pFWZQZZtw=/300x300/v2/https://flxt.tmsimg.com/assets/p175884_k_v9_ae.jpg")!, +// bannerUrl: URL(string: "https://www.sonypictures.com/sites/default/files/styles/max_360x390/public/banner-images/2020-04/stepbrothers_banner_2572x1100_v2.png?h=abc6acbe&itok=7m7ZKbdz")!, +// email: "joe@blow.com" +// ) +// } +// +// func updateUserInfo(firstName: String?, lastName: String?, currentPassword: String?, newPassword: String?, confirmNewPassword: String?) async throws { +// firstName.flatMap { +// guard let currentUser else { return } +// self.currentUser = User(id: currentUser.id, +// createdAt: currentUser.createdAt, +// firstName: $0, +// lastName: currentUser.lastName, +// nickname: currentUser.nickname, +// pictureUrl: currentUser.pictureUrl, +// bannerUrl: currentUser.bannerUrl, +// email: currentUser.email) +// } +// lastName.flatMap { +// guard let currentUser else { return } +// self.currentUser = User(id: currentUser.id, +// createdAt: currentUser.createdAt, +// firstName: currentUser.firstName, +// lastName: $0, +// nickname: currentUser.nickname, +// pictureUrl: currentUser.pictureUrl, +// bannerUrl: currentUser.bannerUrl, +// email: currentUser.email) +// } +// } +// +// init() {} +// +// var currentUser: User? +//} diff --git a/iosApp/Modules/Screens/Home/UI/Profile/ProfileMoreViewModel.swift b/iosApp/Modules/Screens/Home/UI/Profile/ProfileMoreViewModel.swift index 338ac268..996e573d 100644 --- a/iosApp/Modules/Screens/Home/UI/Profile/ProfileMoreViewModel.swift +++ b/iosApp/Modules/Screens/Home/UI/Profile/ProfileMoreViewModel.swift @@ -1,13 +1,14 @@ import Foundation import Data import Resolver +import shared @MainActor class ProfileMoreViewModel: ObservableObject { typealias Row = (title: String, url: String) typealias Section = (title: String, rows: [Row]) - private let loginRepo = LoginRepo.shared + @Injected private var loginUseCase: LoginUseCase var sections: [Section] { [ @@ -24,6 +25,6 @@ class ProfileMoreViewModel: ObservableObject { } func logOut() { - loginRepo.logOut() + loginUseCase.logOut() } } diff --git a/iosApp/Modules/Screens/Home/UI/Profile/ProfileView.swift b/iosApp/Modules/Screens/Home/UI/Profile/ProfileView.swift index 4f0de655..8b83d277 100644 --- a/iosApp/Modules/Screens/Home/UI/Profile/ProfileView.swift +++ b/iosApp/Modules/Screens/Home/UI/Profile/ProfileView.swift @@ -26,7 +26,7 @@ struct ProfileView: View { ZStack { ScrollView { VStack(alignment: .leading) { - HeaderImageSection(viewModel.user?.bannerUrl?.absoluteString) +// HeaderImageSection(viewModel.user?.bannerUrl?.absoluteString) artistImage bottomSection .addSidePadding() @@ -76,7 +76,7 @@ struct ProfileView: View { @ViewBuilder private var artistImage: some View { - AsyncImage(url: viewModel.user?.pictureUrl) + AsyncImage(url: /*viewModel.user?.pictureUrl*/nil) .circle(size: userImageSize) .padding(.top, -(sectionSpacing+userImageSize/2)) } diff --git a/iosApp/Modules/Screens/Home/UI/Profile/ProfileViewModel.swift b/iosApp/Modules/Screens/Home/UI/Profile/ProfileViewModel.swift index 3ee672a2..759e0f9c 100644 --- a/iosApp/Modules/Screens/Home/UI/Profile/ProfileViewModel.swift +++ b/iosApp/Modules/Screens/Home/UI/Profile/ProfileViewModel.swift @@ -4,12 +4,13 @@ import Resolver import Models import ModuleLinker import Combine +import shared @MainActor final class ProfileViewModel: ObservableObject { - @Injected private var userRepo: any UserManaging +// @Injected private var userRepo: any UserManaging - var user: User? { userRepo.currentUser } +// var user: User? { userRepo.currentUser } @Published var firstName: String = "" @Published var lastName: String = "" @@ -26,27 +27,28 @@ final class ProfileViewModel: ObservableObject { var ptrOffset: CGFloat = 0 var showSaveButton: Bool { - guard let user = user else { return false } - - func hasNewPassword() -> Bool { - currentPassword.isEmpty == false && - newPassword.isEmpty == false && - confirmPassword.isEmpty == false - } - - func infoFieldsAreEmpty() -> Bool { - firstName.isEmpty && - lastName.isEmpty && - currentPassword.isEmpty - } - - func hasNewInfo() -> Bool { - firstName != user.firstName || - lastName != user.lastName || - email != user.email - } - - return hasNewPassword() || (infoFieldsAreEmpty() == false && hasNewInfo()) +// guard let user = user else { return false } +// +// func hasNewPassword() -> Bool { +// currentPassword.isEmpty == false && +// newPassword.isEmpty == false && +// confirmPassword.isEmpty == false +// } +// +// func infoFieldsAreEmpty() -> Bool { +// firstName.isEmpty && +// lastName.isEmpty && +// currentPassword.isEmpty +// } +// +// func hasNewInfo() -> Bool { +// firstName != user.firstName || +// lastName != user.lastName || +// email != user.email +// } +// +// return hasNewPassword() || (infoFieldsAreEmpty() == false && hasNewInfo()) + false } init() { @@ -58,37 +60,37 @@ final class ProfileViewModel: ObservableObject { } func loadUser() async { - // don't set loading state here, since this might be called from the view's "refreshable" - do { - try await userRepo.fetchCurrentUser() - updateUserFields() - } catch { - self.error = error.localizedDescription - } +// // don't set loading state here, since this might be called from the view's "refreshable" +// do { +//// try await userRepo.fetchCurrentUser() +// updateUserFields() +// } catch { +// self.error = error.localizedDescription +// } } func save() { Task { isLoading = true - do { - try await userRepo.updateUserInfo( - firstName: firstName, - lastName: lastName, - currentPassword: currentPassword, - newPassword: newPassword, - confirmNewPassword: confirmPassword - ) - } catch { - self.error = error.localizedDescription - } +// do { +//// try await userRepo.updateUserInfo( +//// firstName: firstName, +//// lastName: lastName, +//// currentPassword: currentPassword, +//// newPassword: newPassword, +//// confirmNewPassword: confirmPassword +//// ) +// } catch { +// self.error = error.localizedDescription +// } isLoading = false } } private func updateUserFields() { - guard let user = user else { return } - firstName = user.firstName.emptyIfNil - lastName = user.lastName.emptyIfNil - email = user.email.emptyIfNil +// guard let user = user else { return } +// firstName = user.firstName.emptyIfNil +// lastName = user.lastName.emptyIfNil +// email = user.email.emptyIfNil } } diff --git a/iosApp/Modules/Screens/Login/LoginModule.swift b/iosApp/Modules/Screens/Login/LoginModule.swift index 806ee5a2..0f39dd30 100644 --- a/iosApp/Modules/Screens/Login/LoginModule.swift +++ b/iosApp/Modules/Screens/Login/LoginModule.swift @@ -2,6 +2,7 @@ import Foundation import ModuleLinker import Resolver import SwiftUI +import shared public final class LoginModule: ModuleProtocol { public static var shared = LoginModule() @@ -10,23 +11,20 @@ public final class LoginModule: ModuleProtocol { Resolver.register { self as LoginViewProviding } + + Resolver.register { + LoginUseCaseProvider().get() as LoginUseCase + } + + Resolver.register { + SignupUseCaseProvider().get() as SignupUseCase + } } } #if DEBUG extension LoginModule { public func registerAllMockedServices(mockResolver: Resolver) { -// mockResolver.register { -// MockLogInLogOutUseCase.shared as LoggedInUserUseCaseProtocol -// } - -// mockResolver.register { -// MockLogInLogOutUseCase.shared as LoginRepo -// } - -// mockResolver.register { -// MockLogInLogOutUseCase.shared as LogOutUseCaseProtocol -// } } } #endif diff --git a/iosApp/Modules/Screens/Login/UI/ViewModels/LandingViewModel+ErrorHandling.swift b/iosApp/Modules/Screens/Login/UI/ViewModels/LandingViewModel+ErrorHandling.swift new file mode 100644 index 00000000..e249c818 --- /dev/null +++ b/iosApp/Modules/Screens/Login/UI/ViewModels/LandingViewModel+ErrorHandling.swift @@ -0,0 +1,53 @@ +import Foundation +import shared + +extension LandingViewModel { + func handleError(_ error: Error) { + if let error = error.kmmException { + handleKotlinError(error) + } else { + self.error = error.localizedDescription + } + } + + private func handleKotlinError(_ kmmException: KMMException) { + switch kmmException { + case let kmmException as RegisterException: + handleRegisterException(kmmException) + case let kmmException as LoginException: + handleLoginException(kmmException) + default: + break + } + self.error = kmmException.message + } + + private func handleRegisterException(_ exception: RegisterException) { + switch exception { + case is RegisterException.UserAlreadyExists: + email = "" + navigateBackTo(.createAccount) + case is RegisterException.TwoFactorAuthenticationFailed: + confirmationCode = "" + navigateBackTo(.codeConfirmation) + default: + break + } + } + + private func handleLoginException(_ exception: LoginException) { + switch exception { + case is LoginException.UserNotFound: + email = "" + password = "" + default: + break + } + } +} + +extension String: Error { + var localizedDescription: String { + self + } +} diff --git a/iosApp/Modules/Screens/Login/UI/ViewModels/LandingViewModel.swift b/iosApp/Modules/Screens/Login/UI/ViewModels/LandingViewModel.swift index 9bcaa47d..3053ea00 100644 --- a/iosApp/Modules/Screens/Login/UI/ViewModels/LandingViewModel.swift +++ b/iosApp/Modules/Screens/Login/UI/ViewModels/LandingViewModel.swift @@ -5,6 +5,7 @@ import GoogleSignIn import FacebookLogin import AuthenticationServices import ModuleLinker +import shared @MainActor class LandingViewModel: ObservableObject { @@ -19,9 +20,10 @@ class LandingViewModel: ObservableObject { @Published var confirmPassword: String = "" @Published var nickname: String = "" - private let logInRepo = LoginRepo.shared - @Injected private var userRepo: any UserManaging - private let loginFieldValidator = LoginFieldValidator() + @Injected private var logInUseCase: any LoginUseCase + @Injected private var signUpUseCase: any SignupUseCase +// @Injected private var userRepo: any UserManaging + private let loginFieldValidator = shared.LoginFieldValidator() var nicknameIsValid: Bool { nickname.count > 0 @@ -48,9 +50,9 @@ class LandingViewModel: ObservableObject { isLoading = true Task { do { - try await logInRepo.login(email: email, password: password) + try await logInUseCase.logIn(email: email, password: password) } catch { - self.error = error.localizedDescription + handleError(error) } isLoading = false } @@ -64,22 +66,22 @@ class LandingViewModel: ObservableObject { navPath.append(.enterNewPassword) Task { do { - try await logInRepo.requestEmailVerificationCode(for: email) + try await signUpUseCase.requestEmailConfirmationCode(email: email) } catch { - self.error = error.localizedDescription + handleError(error) } } } func resetPassword() { - Task { - do { - try await userRepo.resetPassword(email: email, password: password, confirmPassword: confirmPassword, authCode: confirmationCode) - try await logInRepo.login(email: email, password: password) - } catch { - self.error = error.localizedDescription - } - } +// Task { +// do { +// try await userRepo.resetPassword(email: email, password: password, confirmPassword: confirmPassword, authCode: confirmationCode) +// try await logInUseCase.logIn(email: email, password: password) +// } catch { +// handleError(error) +// } +// } } func createAccount() { @@ -92,9 +94,9 @@ class LandingViewModel: ObservableObject { } Task { do { - try await logInRepo.requestEmailVerificationCode(for: email) + try await signUpUseCase.requestEmailConfirmationCode(email: email) } catch { - self.error = error.localizedDescription + handleError(error) } } } @@ -107,23 +109,10 @@ class LandingViewModel: ObservableObject { isLoading = true Task { do { - try await userRepo.createUser(nickname: nickname, email: email, password: password, passwordConfirmation: confirmPassword, verificationCode: confirmationCode) + try await signUpUseCase.registerUser(nickname: nickname, email: email, password: password, passwordConfirmation: confirmPassword, verificationCode: confirmationCode) navPath.append(.done) - } catch let error as UserRepoError { - switch error { - case .accountExists: - self.error = "An account with this email already exists." - navigateBackTo(.createAccount) - case .twoFAFailed: - self.error = "You entered the incorrect verification code." - navigateBackTo(.codeConfirmation) - case .dataUnavailable: - self.error = "Failed to fetch data." - case .displayError(let errorString): - self.error = errorString - } } catch { - self.error = error.localizedDescription + handleError(error) } isLoading = false @@ -141,36 +130,45 @@ class LandingViewModel: ObservableObject { func handleFacebookLogin(result: Result) { switch result { case .success(let loginResult): + guard let token = loginResult.token?.tokenString else { + //TODO: localize + self.error = "Failed to log in with Facebook" + return + } isLoading = true Task { do { - try await logInRepo.loginWithFacebook(result: loginResult) + try await logInUseCase.logInWithFacebook(accessToken: token) } catch { self.error = error.localizedDescription } isLoading = false } case .failure(let error): - self.error = error.localizedDescription + handleError(error) } } func handleFacebookLogout() { - logInRepo.logOut() + logInUseCase.logOut() } func handleGoogleSignIn(result: GIDSignInResult?, error: Error?) { - guard let result = result, error == nil else { - self.error = error?.localizedDescription - return + guard let idToken = result?.user.accessToken.tokenString else { + //TODO: localize + handleError("Failed to sign in with Google"); return + } + + guard error == nil else { + handleError(error!); return } isLoading = true Task { do { - try await logInRepo.loginWithGoogle(result: result) + try await logInUseCase.logInWithGoogle(idToken: idToken) } catch { - self.error = error.localizedDescription + handleError(error) } isLoading = false } @@ -179,17 +177,24 @@ class LandingViewModel: ObservableObject { func handleAppleSignIn(result: Result) { switch result { case .success(let authResults): + guard + let identityToken = (authResults.credential as? ASAuthorizationAppleIDCredential)?.identityToken, + let token = String(data: identityToken, encoding: .utf8) + else { + self.error = "Could not sign in with Apple" + return + } isLoading = true Task { do { - try await logInRepo.loginWithApple(result: authResults) + try await logInUseCase.logInWithApple(idToken: token) } catch { - self.error = "\(error)" + handleError(error) } isLoading = false } case .failure(let error): - self.error = "\(error)" + handleError(error) } } } diff --git a/iosApp/Modules/Screens/Login/UI/Views/LandingView.swift b/iosApp/Modules/Screens/Login/UI/Views/LandingView.swift index fb99ae77..13654de4 100644 --- a/iosApp/Modules/Screens/Login/UI/Views/LandingView.swift +++ b/iosApp/Modules/Screens/Login/UI/Views/LandingView.swift @@ -58,7 +58,7 @@ extension LandingView { Group { loginButton createAccountButton - facebookLoginButton +// facebookLoginButton googleSignInButton signInWithAppleButton } @@ -92,15 +92,15 @@ extension LandingView { .borderOverlay(color: NEWMColor.grey500(), radius: 4, width: 2) } - @ViewBuilder - private var facebookLoginButton: some View { - FacebookLoginButton(logInCompletionHandler: viewModel.handleFacebookLogin, logOutCompletionHandler: viewModel.handleFacebookLogout) - .frame(height: socialSignInButtonHeight) - .addSidePadding() - .background(Color(red: 24 / 255, green: 119 / 255, blue: 242 / 255)) - .cornerRadius(4) - .padding(.top) - } +// @ViewBuilder +// private var facebookLoginButton: some View { +// FacebookLoginButton(logInCompletionHandler: viewModel.handleFacebookLogin, logOutCompletionHandler: viewModel.handleFacebookLogout) +// .frame(height: socialSignInButtonHeight) +// .addSidePadding() +// .background(Color(red: 24 / 255, green: 119 / 255, blue: 242 / 255)) +// .cornerRadius(4) +// .padding(.top) +// } @ViewBuilder private var signInWithAppleButton: some View { diff --git a/iosApp/Modules/Screens/Main/UI/MainView.swift b/iosApp/Modules/Screens/Main/UI/MainView.swift index 7514f023..08ade1d8 100644 --- a/iosApp/Modules/Screens/Main/UI/MainView.swift +++ b/iosApp/Modules/Screens/Main/UI/MainView.swift @@ -16,8 +16,6 @@ public struct MainView: View { @State var route: MainViewRoute? - public init() {} - public var body: some View { GeometryReader { geometry in if viewModel.shouldShowLogin { diff --git a/iosApp/Modules/Screens/Main/UI/MainViewModel.swift b/iosApp/Modules/Screens/Main/UI/MainViewModel.swift index 3739616a..b76e22ba 100644 --- a/iosApp/Modules/Screens/Main/UI/MainViewModel.swift +++ b/iosApp/Modules/Screens/Main/UI/MainViewModel.swift @@ -4,18 +4,27 @@ import ModuleLinker import Resolver import Data import API +import shared @MainActor class MainViewModel: ObservableObject { @Published var selectedTab: MainViewModelTab = .home + //This value isn't used, it's just for triggering a view refresh. + @Published var updateLoginState: Bool = false + @Injected private var loginUseCase: LoginUseCase - @Published var shouldShowLogin: Bool = false - - private let loginRepo = LoginRepo.shared - - private var cancelables = Set() + private var cancels = Set() + + var shouldShowLogin: Bool { + loginUseCase.userIsLoggedIn == false + } - init() { - loginRepo.$userIsLoggedIn.map { !$0 }.assign(to: &$shouldShowLogin) + public init() { + NotificationCenter.default.publisher(for: shared.Notification().loginStateChanged) + .receive(on: RunLoop.main) + .sink { [weak self] _ in + self?.updateLoginState.toggle() + } + .store(in: &cancels) } } diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 2154b3a7..3cb86565 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -62,8 +62,6 @@ 7618FD6528164D99007573B4 /* Artist.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7618FD5E28164D99007573B4 /* Artist.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7618FD7728164DA2007573B4 /* Login.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7618FD7128164DA1007573B4 /* Login.framework */; }; 7618FD7828164DA2007573B4 /* Login.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7618FD7128164DA1007573B4 /* Login.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 7618FD8728164DAB007573B4 /* PlaylistList.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7618FD8128164DAB007573B4 /* PlaylistList.framework */; }; - 7618FD8828164DAB007573B4 /* PlaylistList.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7618FD8128164DAB007573B4 /* PlaylistList.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7618FD9728164DBB007573B4 /* NowPlaying.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7618FD9128164DBB007573B4 /* NowPlaying.framework */; }; 7618FD9828164DBB007573B4 /* NowPlaying.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7618FD9128164DBB007573B4 /* NowPlaying.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7618FDA728164DC6007573B4 /* Tips.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7618FDA128164DC6007573B4 /* Tips.framework */; }; @@ -84,6 +82,10 @@ 7618FE0B28164E72007573B4 /* ModuleLinker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76D01D9E28164B2100305605 /* ModuleLinker.framework */; platformFilter = ios; }; 7618FE1A28164E7F007573B4 /* ModuleLinker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76D01D9E28164B2100305605 /* ModuleLinker.framework */; platformFilter = ios; }; 7618FE1F28164E83007573B4 /* ModuleLinker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76D01D9E28164B2100305605 /* ModuleLinker.framework */; platformFilter = ios; }; + 761ADD3B2AE734AA001B56CF /* shared.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 761ADD3A2AE734AA001B56CF /* shared.xcframework */; }; + 761ADD422AE88A56001B56CF /* Notification+KMM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761ADD412AE88A56001B56CF /* Notification+KMM.swift */; }; + 761ADD442AEA0746001B56CF /* Error+KMMException.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761ADD432AEA0746001B56CF /* Error+KMMException.swift */; }; + 761ADD472AEA0ECF001B56CF /* LandingViewModel+ErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761ADD452AEA0EAA001B56CF /* LandingViewModel+ErrorHandling.swift */; }; 7625CE2F294088110033C669 /* ArtistCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7625CE2E294088110033C669 /* ArtistCell.swift */; }; 7625CE31294089990033C669 /* AudioPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C76132291F445B0054D2F3 /* AudioPlayer.framework */; }; 7625CE3529408A3B0033C669 /* Colors.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7618FD1728164CBD007573B4 /* Colors.framework */; }; @@ -218,7 +220,6 @@ 76A24DE72A3FE76900559460 /* APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76A24DE62A3FE76900559460 /* APIError.swift */; }; 76A24DE92A3FEDD300559460 /* DataModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76A24DE82A3FEDD300559460 /* DataModule.swift */; }; 76A24DED2A3FF07E00559460 /* Resolver in Frameworks */ = {isa = PBXBuildFile; productRef = 76A24DEC2A3FF07E00559460 /* Resolver */; }; - 76A24DEE2A3FF22300559460 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76A24DEA2A3FEF7E00559460 /* Data.swift */; }; 76A24DEF2A3FF26600559460 /* ModuleLinker.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76D01D9E28164B2100305605 /* ModuleLinker.framework */; }; 76A24DF02A3FF26600559460 /* ModuleLinker.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 76D01D9E28164B2100305605 /* ModuleLinker.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 76A24DF52A4026A000559460 /* MockUserManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76A24DF42A4026A000559460 /* MockUserManager.swift */; }; @@ -243,12 +244,8 @@ 76B21D13293F4B17003692F7 /* HorizontalScrollingGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B21D12293F4B17003692F7 /* HorizontalScrollingGridView.swift */; }; 76B43C6627D875E900F21076 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 76B43C6727D875E900F21076 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; - 76B52D4829F8688200EB6357 /* Data.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76B52D4229F8688200EB6357 /* Data.framework */; }; - 76B52D4929F8688200EB6357 /* Data.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 76B52D4229F8688200EB6357 /* Data.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 76B52D5629F86B7200EB6357 /* Data.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76B52D4229F8688200EB6357 /* Data.framework */; }; 76B52D5E29F86C0400EB6357 /* LoginRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B52D5D29F86C0400EB6357 /* LoginRepo.swift */; }; - 76B52D6A29F86D4100EB6357 /* API.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76B52D6429F86D4000EB6357 /* API.framework */; }; - 76B52D6B29F86D4100EB6357 /* API.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 76B52D6429F86D4000EB6357 /* API.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 76B52D7229F86F0800EB6357 /* API.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76B52D6429F86D4000EB6357 /* API.framework */; }; 76B52D7929F8793600EB6357 /* Data.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76B52D4229F8688200EB6357 /* Data.framework */; }; 76B52D8229FE4F0F00EB6357 /* FacebookLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 76B52D8129FE4F0F00EB6357 /* FacebookLogin */; }; @@ -426,13 +423,6 @@ remoteGlobalIDString = 7618FD7028164DA1007573B4; remoteInfo = Login; }; - 7618FD8528164DAB007573B4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7555FF73242A565900829871 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 7618FD8028164DAB007573B4; - remoteInfo = PlaylistList; - }; 7618FD9528164DBB007573B4 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7555FF73242A565900829871 /* Project object */; @@ -713,13 +703,6 @@ remoteGlobalIDString = 7555FF7A242A565900829871; remoteInfo = iosApp; }; - 76B52D4629F8688200EB6357 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7555FF73242A565900829871 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 76B52D4129F8688200EB6357; - remoteInfo = Auth; - }; 76B52D5829F86B7200EB6357 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7555FF73242A565900829871 /* Project object */; @@ -727,13 +710,6 @@ remoteGlobalIDString = 76B52D4129F8688200EB6357; remoteInfo = Auth; }; - 76B52D6829F86D4100EB6357 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7555FF73242A565900829871 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 76B52D6329F86D4000EB6357; - remoteInfo = API; - }; 76B52D7429F86F0800EB6357 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7555FF73242A565900829871 /* Project object */; @@ -818,13 +794,6 @@ remoteGlobalIDString = 7555FF7A242A565900829871; remoteInfo = iosApp; }; - 76CB88E828A275F800878D8C /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7555FF73242A565900829871 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 76CB88C328A273DB00878D8C; - remoteInfo = SharedExtensions; - }; 76D01D4328164A8400305605 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7555FF73242A565900829871 /* Project object */; @@ -905,7 +874,6 @@ dstSubfolderSpec = 10; files = ( 7618FD6528164D99007573B4 /* Artist.framework in Embed Frameworks */, - 76B52D6B29F86D4100EB6357 /* API.framework in Embed Frameworks */, 7618FCD428164C5C007573B4 /* SharedUI.framework in Embed Frameworks */, 76C76139291F445B0054D2F3 /* AudioPlayer.framework in Embed Frameworks */, 7618FD9828164DBB007573B4 /* NowPlaying.framework in Embed Frameworks */, @@ -917,8 +885,6 @@ 7618FCF528164C86007573B4 /* TabBar.framework in Embed Frameworks */, 76D01DA528164B2100305605 /* ModuleLinker.framework in Embed Frameworks */, 76D01D4628164A8400305605 /* Home.framework in Embed Frameworks */, - 7618FD8828164DAB007573B4 /* PlaylistList.framework in Embed Frameworks */, - 76B52D4929F8688200EB6357 /* Data.framework in Embed Frameworks */, 76D01D8028164AE700305605 /* Main.framework in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -1014,6 +980,10 @@ 7618FDDB28164E23007573B4 /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 7618FDDD28164E32007573B4 /* WalletModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletModule.swift; sourceTree = ""; }; 7618FDDE28164E32007573B4 /* WalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletView.swift; sourceTree = ""; }; + 761ADD3A2AE734AA001B56CF /* shared.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = shared.xcframework; path = ../shared/build/XCFrameworks/debug/shared.xcframework; sourceTree = ""; }; + 761ADD412AE88A56001B56CF /* Notification+KMM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+KMM.swift"; sourceTree = ""; }; + 761ADD432AEA0746001B56CF /* Error+KMMException.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+KMMException.swift"; sourceTree = ""; }; + 761ADD452AEA0EAA001B56CF /* LandingViewModel+ErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LandingViewModel+ErrorHandling.swift"; sourceTree = ""; }; 7625CE2E294088110033C669 /* ArtistCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistCell.swift; sourceTree = ""; }; 7625D14D29429CCC0033C669 /* Filters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filters.swift; sourceTree = ""; }; 7625D1512942B0740033C669 /* LibraryRoutes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryRoutes.swift; sourceTree = ""; }; @@ -1219,17 +1189,15 @@ 76D01D4528164A8400305605 /* Home.framework in Frameworks */, 76D01DA428164B2100305605 /* ModuleLinker.framework in Frameworks */, 76C76138291F445B0054D2F3 /* AudioPlayer.framework in Frameworks */, - 76B52D6A29F86D4100EB6357 /* API.framework in Frameworks */, 7618FD7728164DA2007573B4 /* Login.framework in Frameworks */, 7618FD3728164CE2007573B4 /* Fonts.framework in Frameworks */, 7618FCF428164C86007573B4 /* TabBar.framework in Frameworks */, 76562530284BF6E7004B6E4B /* Library.framework in Frameworks */, 76D01D7F28164AE700305605 /* Main.framework in Frameworks */, + 761ADD3B2AE734AA001B56CF /* shared.xcframework in Frameworks */, 7618FCD328164C5C007573B4 /* SharedUI.framework in Frameworks */, 7618FD1D28164CBD007573B4 /* Colors.framework in Frameworks */, - 76B52D4829F8688200EB6357 /* Data.framework in Frameworks */, 76F823C528BEC2B3003A524C /* Resolver in Frameworks */, - 7618FD8728164DAB007573B4 /* PlaylistList.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1490,6 +1458,7 @@ 7555FF72242A565900829871 = { isa = PBXGroup; children = ( + 761ADD3A2AE734AA001B56CF /* shared.xcframework */, 76EAFC5C2A0AE33E00ACD862 /* Secrets.xcconfig */, 76B990BB2835203300F818E4 /* Fonts-Info.plist */, 7627329028295E8C009D3C8A /* Documentation */, @@ -1564,9 +1533,11 @@ 760C2C7128165D920030ACF4 /* Extensions */ = { isa = PBXGroup; children = ( - 7618FCDC28164C6B007573B4 /* UIImage+Extensions.swift */, + 761ADD432AEA0746001B56CF /* Error+KMMException.swift */, + 761ADD412AE88A56001B56CF /* Notification+KMM.swift */, 76DB3B972A13252F001AF2AD /* String+NilIfEmpty.swift */, 760C2C7228165DE80030ACF4 /* SwiftUIView+Erased.swift */, + 7618FCDC28164C6B007573B4 /* UIImage+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -1893,12 +1864,12 @@ 765A35E829E9052E00B939BD /* CodeConfirmationView.swift */, 765A35E429E9052E00B939BD /* CreateAccountView.swift */, 765A35EB29E9052E00B939BD /* DoneView.swift */, + 76EAFC512A08F09600ACD862 /* EnterNewPasswordView.swift */, + 76FB0E6429F6695000C23F18 /* FacebookLoginButton.swift */, + 76EAFC4F2A08ED0900ACD862 /* ForgotPasswordView.swift */, 765A35DE29E9052E00B939BD /* LandingView.swift */, 765A35F529E9052E00B939BD /* LoginView.swift */, 765A35F129E9052E00B939BD /* UsernameView.swift */, - 76FB0E6429F6695000C23F18 /* FacebookLoginButton.swift */, - 76EAFC4F2A08ED0900ACD862 /* ForgotPasswordView.swift */, - 76EAFC512A08F09600ACD862 /* EnterNewPasswordView.swift */, ); path = Views; sourceTree = ""; @@ -2288,8 +2259,8 @@ 76CE046529EE3F13002AF94B /* Utilities */ = { isa = PBXGroup; children = ( - 766B1EC62A15C74B0083DE3E /* KeyboardObserver.swift */, 76CE046629EE3F30002AF94B /* Binding.swift */, + 766B1EC62A15C74B0083DE3E /* KeyboardObserver.swift */, ); path = Utilities; sourceTree = ""; @@ -2297,6 +2268,7 @@ 76CE046A29EE6596002AF94B /* ViewModels */ = { isa = PBXGroup; children = ( + 761ADD452AEA0EAA001B56CF /* LandingViewModel+ErrorHandling.swift */, 765A35DF29E9052E00B939BD /* LandingViewModel.swift */, ); path = ViewModels; @@ -2581,14 +2553,10 @@ 7618FD1C28164CBD007573B4 /* PBXTargetDependency */, 7618FD3628164CE2007573B4 /* PBXTargetDependency */, 7618FD7628164DA2007573B4 /* PBXTargetDependency */, - 7618FD8628164DAB007573B4 /* PBXTargetDependency */, 7618FD9628164DBB007573B4 /* PBXTargetDependency */, 7618FDA628164DC6007573B4 /* PBXTargetDependency */, 76562519284BEF30004B6E4B /* PBXTargetDependency */, - 76CB88E928A275F800878D8C /* PBXTargetDependency */, 76C76137291F445B0054D2F3 /* PBXTargetDependency */, - 76B52D4729F8688200EB6357 /* PBXTargetDependency */, - 76B52D6929F86D4100EB6357 /* PBXTargetDependency */, ); name = iosApp; packageProductDependencies = ( @@ -3115,6 +3083,7 @@ isa = PBXNativeTarget; buildConfigurationList = 76D01DA628164B2100305605 /* Build configuration list for PBXNativeTarget "ModuleLinker" */; buildPhases = ( + 761ADD102AE389F0001B56CF /* ShellScript */, 76D01D9A28164B2100305605 /* Sources */, 76D01D9928164B2100305605 /* Headers */, 76D01D9B28164B2100305605 /* Frameworks */, @@ -3508,6 +3477,23 @@ shellPath = /bin/sh; shellScript = "#swiftgen\ncd Modules/Resources/Colors\nswiftgen\n"; }; + 761ADD102AE389F0001B56CF /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/bash; + shellScript = "#cd \"$SRCROOT/..\"\n#./gradlew :shared:assembleXCFramework\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -3628,6 +3614,7 @@ 76FB0E6529F6695000C23F18 /* FacebookLoginButton.swift in Sources */, 765A360529E9052E00B939BD /* LoginMocks.swift in Sources */, 765A361729E9052E00B939BD /* LoginView.swift in Sources */, + 761ADD472AEA0ECF001B56CF /* LandingViewModel+ErrorHandling.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3833,8 +3820,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 761ADD422AE88A56001B56CF /* Notification+KMM.swift in Sources */, 76562535284BF758004B6E4B /* Library.swift in Sources */, 76764AF3281D015B0095BEF9 /* Playlist.swift in Sources */, + 761ADD442AEA0746001B56CF /* Error+KMMException.swift in Sources */, 76D9F1F92A1714E300C93E1C /* KeyboardObserver.swift in Sources */, 760C2C7D281660F70030ACF4 /* Tips.swift in Sources */, 76DC3A4D281A0F1D004D5021 /* ModuleLinkerModule.swift in Sources */, @@ -3846,7 +3835,6 @@ 7618FCC228164BB5007573B4 /* ModuleProtocol.swift in Sources */, 76CE046729EE3F30002AF94B /* Binding.swift in Sources */, 76C76147291F4D8D0054D2F3 /* AudioPlayer.swift in Sources */, - 76A24DEE2A3FF22300559460 /* Data.swift in Sources */, 760C2C7528165F450030ACF4 /* NowPlaying.swift in Sources */, 76DC3A1928167276004D5021 /* UIImage+Extensions.swift in Sources */, 761837AB294A706F0033250B /* Artist.swift in Sources */, @@ -3927,11 +3915,6 @@ target = 7618FD7028164DA1007573B4 /* Login */; targetProxy = 7618FD7528164DA2007573B4 /* PBXContainerItemProxy */; }; - 7618FD8628164DAB007573B4 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 7618FD8028164DAB007573B4 /* PlaylistList */; - targetProxy = 7618FD8528164DAB007573B4 /* PBXContainerItemProxy */; - }; 7618FD9628164DBB007573B4 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 7618FD9028164DBB007573B4 /* NowPlaying */; @@ -4159,21 +4142,11 @@ target = 7555FF7A242A565900829871 /* iosApp */; targetProxy = 76ADDCF328981B03008AA39A /* PBXContainerItemProxy */; }; - 76B52D4729F8688200EB6357 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 76B52D4129F8688200EB6357 /* Data */; - targetProxy = 76B52D4629F8688200EB6357 /* PBXContainerItemProxy */; - }; 76B52D5929F86B7200EB6357 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 76B52D4129F8688200EB6357 /* Data */; targetProxy = 76B52D5829F86B7200EB6357 /* PBXContainerItemProxy */; }; - 76B52D6929F86D4100EB6357 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 76B52D6329F86D4000EB6357 /* API */; - targetProxy = 76B52D6829F86D4100EB6357 /* PBXContainerItemProxy */; - }; 76B52D7529F86F0800EB6357 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 76B52D6329F86D4000EB6357 /* API */; @@ -4236,11 +4209,6 @@ target = 7555FF7A242A565900829871 /* iosApp */; targetProxy = 76CB88D128A273DB00878D8C /* PBXContainerItemProxy */; }; - 76CB88E928A275F800878D8C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - platformFilter = ios; - targetProxy = 76CB88E828A275F800878D8C /* PBXContainerItemProxy */; - }; 76D01D4428164A8400305605 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 76D01D3E28164A8400305605 /* Home */; @@ -4464,6 +4432,7 @@ "\"$(SRCROOT)/Modules\"/**", ); INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NEWM; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -4498,6 +4467,7 @@ "\"$(SRCROOT)/Modules\"/**", ); INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NEWM; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f6c17459..63031fcf 100644 --- a/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/facebook/facebook-ios-sdk", "state" : { - "revision" : "2ef0af2c7d8b6d6c3a14edf5299b78730555c679", - "version" : "16.1.0" + "revision" : "ebfaa5eb867c9ad3e0b54ef8b03883f024cfc18a", + "version" : "16.2.1" } }, { diff --git a/iosApp/iosApp/App/iOSApp.swift b/iosApp/iosApp/App/iOSApp.swift index f782f7cf..4776cbea 100644 --- a/iosApp/iosApp/App/iOSApp.swift +++ b/iosApp/iosApp/App/iOSApp.swift @@ -4,12 +4,14 @@ import ModuleLinker import FacebookCore import Data import UIKit +import shared @main struct iOSApp: App { let mainViewProvider: MainViewProviding init() { + KoinKt.doInitKoin() #if DEBUG UserDefaults.standard.set(false, forKey: "_UIConstraintBasedLayoutLogUnsatisfiable") #endif diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 201b5646..a8eed08a 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -33,8 +33,8 @@ kotlin { val xcf = XCFramework() listOf( - iosX64(), - iosArm64(), +// iosX64(), +// iosArm64(), iosSimulatorArm64() ).forEach { it.binaries.framework { @@ -56,6 +56,7 @@ kotlin { implementation(Ktor.clientContentNegotiation) implementation(Ktor.kotlinXJson) implementation(Ktor.clientAuth) + implementation("com.liftric:kvault:1.12.0") } } val commonTest by getting { @@ -80,26 +81,26 @@ kotlin { } } - val iosX64Main by getting - val iosArm64Main by getting +// val iosX64Main by getting +// val iosArm64Main by getting val iosSimulatorArm64Main by getting val iosMain by creating { dependsOn(commonMain) - iosX64Main.dependsOn(this) - iosArm64Main.dependsOn(this) +// iosX64Main.dependsOn(this) +// iosArm64Main.dependsOn(this) iosSimulatorArm64Main.dependsOn(this) dependencies { implementation(Ktor.iosDarwin) implementation(SqlDelight.nativeDriver) } } - val iosX64Test by getting - val iosArm64Test by getting +// val iosX64Test by getting +// val iosArm64Test by getting val iosSimulatorArm64Test by getting val iosTest by creating { dependsOn(commonTest) - iosX64Test.dependsOn(this) - iosArm64Test.dependsOn(this) +// iosX64Test.dependsOn(this) +// iosArm64Test.dependsOn(this) iosSimulatorArm64Test.dependsOn(this) } } diff --git a/shared/src/androidMain/kotlin/shared/NotificationCenter.kt b/shared/src/androidMain/kotlin/shared/NotificationCenter.kt new file mode 100644 index 00000000..4682616b --- /dev/null +++ b/shared/src/androidMain/kotlin/shared/NotificationCenter.kt @@ -0,0 +1,6 @@ +package shared + +actual fun postNotification(name: String) { + //TODO + println("postNotification: $name") +} diff --git a/shared/src/androidMain/kotlin/shared/SecureStorage.kt b/shared/src/androidMain/kotlin/shared/SecureStorage.kt deleted file mode 100644 index 37a1d2ac..00000000 --- a/shared/src/androidMain/kotlin/shared/SecureStorage.kt +++ /dev/null @@ -1,38 +0,0 @@ -package shared - -import android.content.Context -import android.content.SharedPreferences -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey - -actual class SecureStorage(private val context: Context) { - private val masterKey = MasterKey.Builder(context) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .build() - - private val sharedPreferences: SharedPreferences = EncryptedSharedPreferences.create( - context, - "newm_encrypted_shared_prefs", - masterKey, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - - actual fun store(key: String, value: String) { - with(sharedPreferences.edit()) { - putString(key, value) - apply() - } - } - - actual fun retrieve(key: String): String? { - return sharedPreferences.getString(key, null) - } - - actual fun remove(key: String) { - with(sharedPreferences.edit()) { - remove(key) - apply() - } - } -} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/shared/actual.kt b/shared/src/androidMain/kotlin/shared/actual.kt index 8e6d9d9a..c876c15e 100644 --- a/shared/src/androidMain/kotlin/shared/actual.kt +++ b/shared/src/androidMain/kotlin/shared/actual.kt @@ -1,9 +1,10 @@ package shared +import com.liftric.kvault.KVault import com.squareup.sqldelight.android.AndroidSqliteDriver -import io.ktor.client.engine.android.* -import io.newm.shared.db.cache.NewmDatabase +import io.ktor.client.engine.android.Android import io.newm.shared.db.NewmDatabaseWrapper +import io.newm.shared.db.cache.NewmDatabase import org.koin.dsl.module actual fun platformModule() = module { @@ -12,5 +13,5 @@ actual fun platformModule() = module { NewmDatabaseWrapper(NewmDatabase(driver)) } single { Android.create() } - single { SecureStorage(get()) } + single { KVault(get(), "user-account") } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/io.newm.shared/TokenManagerImpl.kt b/shared/src/commonMain/kotlin/io.newm.shared/TokenManagerImpl.kt index 9d5869e0..4ac5204c 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/TokenManagerImpl.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/TokenManagerImpl.kt @@ -1,12 +1,12 @@ package io.newm.shared import co.touchlab.kermit.Logger +import com.liftric.kvault.KVault import org.koin.core.component.KoinComponent import org.koin.core.component.inject -import shared.SecureStorage internal class TokenManagerImpl() : KoinComponent, TokenManager { - private val storage: SecureStorage by inject() + private val storage: KVault by inject() private val logger = Logger.withTag("NewmKMM-TokenManagerImpl") override fun hasTokens(): Boolean { @@ -14,27 +14,27 @@ internal class TokenManagerImpl() : KoinComponent, TokenManager { } override fun getAccessToken(): String? { - return storage.retrieve(ACCESS_TOKEN_KEY) ?: run { + return storage.string(ACCESS_TOKEN_KEY) ?: run { logger.d("No Access Token found - Time to Login") null } } override fun getRefreshToken(): String? { - return storage.retrieve(REFRESH_TOKEN_KEY) ?: run { + return storage.string(REFRESH_TOKEN_KEY) ?: run { logger.d("No Refresh Token found - Time to Login") null } } override fun clearToken() { - storage.remove(ACCESS_TOKEN_KEY) - storage.remove(REFRESH_TOKEN_KEY) + storage.deleteObject(ACCESS_TOKEN_KEY) + storage.deleteObject(REFRESH_TOKEN_KEY) } override fun setAuthTokens(accessToken: String, refreshToken: String) { - storage.store(ACCESS_TOKEN_KEY, accessToken) - storage.store(REFRESH_TOKEN_KEY, refreshToken) + storage.set(ACCESS_TOKEN_KEY, accessToken) + storage.set(REFRESH_TOKEN_KEY, refreshToken) } companion object { diff --git a/shared/src/commonMain/kotlin/io.newm.shared/di/Koin.kt b/shared/src/commonMain/kotlin/io.newm.shared/di/Koin.kt index 2adeb5b5..99713689 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/di/Koin.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/di/Koin.kt @@ -1,6 +1,6 @@ package io.newm.shared.di -import io.ktor.client.engine.* +import io.ktor.client.engine.HttpClientEngine import io.newm.shared.TokenManager import io.newm.shared.TokenManagerImpl import io.newm.shared.login.UserSession @@ -8,12 +8,11 @@ import io.newm.shared.login.UserSessionImpl import io.newm.shared.login.repository.LogInRepository import io.newm.shared.login.repository.LogInRepositoryImpl import io.newm.shared.login.service.LoginAPI -import io.newm.shared.usecases.LoginUseCase -import io.newm.shared.usecases.LoginUseCaseImpl -import io.newm.shared.usecases.SignupUseCase -import io.newm.shared.usecases.SignupUseCaseImpl -import io.newm.shared.repositories.* +import io.newm.shared.repositories.CardanoWalletRepository +import io.newm.shared.repositories.CardanoWalletRepositoryImpl +import io.newm.shared.repositories.GenresRepository import io.newm.shared.repositories.GenresRepositoryImpl +import io.newm.shared.repositories.PlaylistRepository import io.newm.shared.repositories.PlaylistRepositoryImpl import io.newm.shared.repositories.UserRepository import io.newm.shared.repositories.UserRepositoryImpl @@ -23,6 +22,10 @@ import io.newm.shared.services.UserAPI import io.newm.shared.services.CardanoWalletAPI import io.newm.shared.usecases.GetGenresUseCase import io.newm.shared.usecases.GetGenresUseCaseImpl +import io.newm.shared.usecases.LoginUseCase +import io.newm.shared.usecases.LoginUseCaseImpl +import io.newm.shared.usecases.SignupUseCase +import io.newm.shared.usecases.SignupUseCaseImpl import io.newm.shared.usecases.WalletNFTSongsUseCase import io.newm.shared.usecases.WalletNFTSongsUseCaseImpl import kotlinx.coroutines.CoroutineScope @@ -50,7 +53,6 @@ fun commonModule(enableNetworkLogs: Boolean) = module { single { CoroutineScope(Dispatchers.Default + SupervisorJob()) } // Internal API Services single { LoginAPI(get()) } - single { LoginAPI(get()) } single { GenresAPI(get()) } single { UserAPI(get()) } single { PlaylistAPI(get()) } diff --git a/shared/src/commonMain/kotlin/io.newm.shared/login/models/LoginResponse.kt b/shared/src/commonMain/kotlin/io.newm.shared/login/models/LoginResponse.kt index 8abc8f23..ec09757c 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/login/models/LoginResponse.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/login/models/LoginResponse.kt @@ -11,7 +11,6 @@ data class LoginResponse( sealed class LoginException(message: String): KMMException(message) { data class WrongPassword(override val message: String) : LoginException(message) - data class UserNotFound(override val message: String) : LoginException(message) } @@ -21,6 +20,5 @@ fun LoginResponse.isValid(): Boolean { sealed class RegisterException(message: String): KMMException(message) { data class UserAlreadyExists(override val message: String) : RegisterException(message) - data class TwoFactorAuthenticationFailed(override val message: String) : RegisterException(message) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/io.newm.shared/login/models/Users.kt b/shared/src/commonMain/kotlin/io.newm.shared/login/models/Users.kt index 8f98b120..85b13320 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/login/models/Users.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/login/models/Users.kt @@ -9,7 +9,16 @@ data class LogInUser( ) @Serializable -data class GoogleSignInRequest(val idToken: String) +data class GoogleSignInRequest(val accessToken: String) + +@Serializable +data class AppleSignInRequest(val idToken: String) + +@Serializable +data class FacebookSignInRequest(val accessToken: String) + +@Serializable +data class LinkedInSignInRequest(val accessToken: String) @Serializable data class NewUser( diff --git a/shared/src/commonMain/kotlin/io.newm.shared/login/repository/LogInRepository.kt b/shared/src/commonMain/kotlin/io.newm.shared/login/repository/LogInRepository.kt index e29cf937..078bea75 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/login/repository/LogInRepository.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/login/repository/LogInRepository.kt @@ -7,11 +7,13 @@ import io.newm.shared.login.models.* import io.newm.shared.login.service.LoginAPI import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import shared.Notification +import shared.postNotification import kotlin.coroutines.cancellation.CancellationException open class KMMException(message: String) : Throwable(message) -interface LogInRepository { +internal interface LogInRepository { @Throws(Exception::class) suspend fun requestEmailConfirmationCode(email: String) @@ -21,6 +23,10 @@ interface LogInRepository { @Throws(KMMException::class, CancellationException::class) suspend fun logIn(email: String, password: String) + fun logOut() + + fun userIsLoggedIn(): Boolean + @Throws(KMMException::class, CancellationException::class) suspend fun oAuthLogin(oAuthData: OAuthData) @@ -60,19 +66,31 @@ internal class LogInRepositoryImpl : KoinComponent, LogInRepository { return handleLoginResponse { service.logIn(LogInUser(email = email, password = password)) } } + override fun logOut() { + tokenManager.clearToken() + postNotification(Notification.loginStateChanged) + } + + override fun userIsLoggedIn(): Boolean { + return tokenManager.getAccessToken() != null + } + @Throws(KMMException::class, CancellationException::class) override suspend fun oAuthLogin(oAuthData: OAuthData) = handleLoginResponse { logger.d { "logIn: oAuth" } when (oAuthData) { - is OAuthData.Facebook -> TODO() - is OAuthData.Google -> service.loginWithGoogle(GoogleSignInRequest(idToken = oAuthData.idToken)) - is OAuthData.LinkedIn -> TODO() + is OAuthData.Facebook -> service.loginWithFacebook(FacebookSignInRequest(accessToken = oAuthData.accessToken)) + is OAuthData.Google -> service.loginWithGoogle(GoogleSignInRequest(accessToken = oAuthData.idToken)) + is OAuthData.Apple -> service.loginWithApple(AppleSignInRequest(idToken = oAuthData.idToken)) + is OAuthData.LinkedIn -> service.loginWithLinkedIn(LinkedInSignInRequest(accessToken = oAuthData.accessToken)) } } + @Throws(KMMException::class, CancellationException::class) private suspend fun handleLoginResponse(request: suspend () -> LoginResponse) { try { storeAccessToken(request()) + postNotification(Notification.loginStateChanged) } catch (e: ClientRequestException) { when (e.response.status.value) { 404 -> { diff --git a/shared/src/commonMain/kotlin/io.newm.shared/login/repository/OAuthData.kt b/shared/src/commonMain/kotlin/io.newm.shared/login/repository/OAuthData.kt index eede53c0..6ba53415 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/login/repository/OAuthData.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/login/repository/OAuthData.kt @@ -4,4 +4,5 @@ sealed interface OAuthData { data class Google(val idToken: String) : OAuthData data class Facebook(val accessToken: String) : OAuthData data class LinkedIn(val accessToken: String) : OAuthData + data class Apple(val idToken: String) : OAuthData } diff --git a/shared/src/commonMain/kotlin/io.newm.shared/login/service/LoginAPI.kt b/shared/src/commonMain/kotlin/io.newm.shared/login/service/LoginAPI.kt index 6cb7fcb3..356417b0 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/login/service/LoginAPI.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/login/service/LoginAPI.kt @@ -12,7 +12,10 @@ import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.http.contentType import io.newm.shared.di.NetworkClientFactory +import io.newm.shared.login.models.AppleSignInRequest +import io.newm.shared.login.models.FacebookSignInRequest import io.newm.shared.login.models.GoogleSignInRequest +import io.newm.shared.login.models.LinkedInSignInRequest import io.newm.shared.login.models.LogInUser import io.newm.shared.login.models.LoginResponse import io.newm.shared.login.models.NewUser @@ -85,6 +88,51 @@ internal class LoginAPI( } } + @Throws(KMMException::class, CancellationException::class) + suspend fun loginWithApple(request: AppleSignInRequest): LoginResponse { + val response = httpClient.post("/v1/auth/login/apple") { + contentType(ContentType.Application.Json) + setBody(request) + } + + return when (response.status) { + HttpStatusCode.OK -> response.body() + else -> { + throw KMMException("HTTP Error ${response.status}: ${response.bodyAsText()}") + } + } + } + + @Throws(KMMException::class, CancellationException::class) + suspend fun loginWithFacebook(request: FacebookSignInRequest): LoginResponse { + val response = httpClient.post("/v1/auth/login/facebook") { + contentType(ContentType.Application.Json) + setBody(request) + } + + return when (response.status) { + HttpStatusCode.OK -> response.body() + else -> { + throw KMMException("HTTP Error ${response.status}: ${response.bodyAsText()}") + } + } + } + + @Throws(KMMException::class, CancellationException::class) + suspend fun loginWithLinkedIn(request: LinkedInSignInRequest): LoginResponse { + val response = httpClient.post("/v1/auth/login/linkedin") { + contentType(ContentType.Application.Json) + setBody(request) + } + + return when (response.status) { + HttpStatusCode.OK -> response.body() + else -> { + throw KMMException("HTTP Error ${response.status}: ${response.bodyAsText()}") + } + } + } + @Throws(KMMException::class, CancellationException::class) suspend fun resetPassword( email: String, diff --git a/shared/src/commonMain/kotlin/io.newm.shared/usecases/GetCurrentUserUseCase.kt b/shared/src/commonMain/kotlin/io.newm.shared/usecases/GetCurrentUserUseCase.kt new file mode 100644 index 00000000..fb295f92 --- /dev/null +++ b/shared/src/commonMain/kotlin/io.newm.shared/usecases/GetCurrentUserUseCase.kt @@ -0,0 +1,4 @@ +package io.newm.shared.usecases + +class GetCurrentUserUseCase { +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/io.newm.shared/usecases/LoginUseCase.kt b/shared/src/commonMain/kotlin/io.newm.shared/usecases/LoginUseCase.kt index d68abe90..e2ca83ff 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/usecases/LoginUseCase.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/usecases/LoginUseCase.kt @@ -2,11 +2,29 @@ package io.newm.shared.usecases import io.newm.shared.login.repository.KMMException import io.newm.shared.login.repository.LogInRepository +import io.newm.shared.login.repository.OAuthData +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import kotlin.coroutines.cancellation.CancellationException interface LoginUseCase { @Throws(KMMException::class, CancellationException::class) suspend fun logIn(email: String, password: String) + + @Throws(KMMException::class, CancellationException::class) + suspend fun logInWithGoogle(idToken: String) + + @Throws(KMMException::class, CancellationException::class) + suspend fun logInWithFacebook(accessToken: String) + + @Throws(KMMException::class, CancellationException::class) + suspend fun logInWithLinkedIn(accessToken: String) + + @Throws(KMMException::class, CancellationException::class) + suspend fun logInWithApple(idToken: String) + + fun logOut() + val userIsLoggedIn: Boolean } internal class LoginUseCaseImpl(private val repository: LogInRepository) : LoginUseCase { @@ -14,4 +32,39 @@ internal class LoginUseCaseImpl(private val repository: LogInRepository) : Login override suspend fun logIn(email: String, password: String) { return repository.logIn(email = email, password = password) } + + @Throws(KMMException::class, CancellationException::class) + override suspend fun logInWithGoogle(idToken: String) { + return repository.oAuthLogin(OAuthData.Google(idToken)) + } + + @Throws(KMMException::class, CancellationException::class) + override suspend fun logInWithFacebook(accessToken: String) { + return repository.oAuthLogin(OAuthData.Facebook(accessToken)) + } + + @Throws(KMMException::class, CancellationException::class) + override suspend fun logInWithLinkedIn(accessToken: String) { + return repository.oAuthLogin(OAuthData.LinkedIn(accessToken)) + } + + @Throws(KMMException::class, CancellationException::class) + override suspend fun logInWithApple(idToken: String) { + return repository.oAuthLogin(OAuthData.Apple(idToken)) + } + + override fun logOut() { + repository.logOut() + } + + override val userIsLoggedIn: Boolean + get() = repository.userIsLoggedIn() +} + +class LoginUseCaseProvider(): KoinComponent { + private val loginUseCase: LoginUseCase by inject() + + fun get(): LoginUseCase { + return this.loginUseCase + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/io.newm.shared/usecases/SignupUseCase.kt b/shared/src/commonMain/kotlin/io.newm.shared/usecases/SignupUseCase.kt index 6bdf6317..b3d881bf 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/usecases/SignupUseCase.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/usecases/SignupUseCase.kt @@ -3,6 +3,8 @@ package io.newm.shared.usecases import io.newm.shared.login.models.NewUser import io.newm.shared.login.repository.KMMException import io.newm.shared.login.repository.LogInRepository +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject import kotlin.coroutines.cancellation.CancellationException interface SignupUseCase { @@ -43,5 +45,12 @@ internal class SignupUseCaseImpl(private val repository: LogInRepository) : Sign ) repository.registerUser(newUser) } - } + +class SignupUseCaseProvider(): KoinComponent { + private val signUpUseCase: SignupUseCase by inject() + + fun get(): SignupUseCase { + return signUpUseCase + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/io.newm.shared/usecases/WalletNFTSongsUseCase.kt b/shared/src/commonMain/kotlin/io.newm.shared/usecases/WalletNFTSongsUseCase.kt index 6e6090f4..3fe2ba05 100644 --- a/shared/src/commonMain/kotlin/io.newm.shared/usecases/WalletNFTSongsUseCase.kt +++ b/shared/src/commonMain/kotlin/io.newm.shared/usecases/WalletNFTSongsUseCase.kt @@ -1,15 +1,10 @@ package io.newm.shared.usecases -import co.touchlab.kermit.Logger -import io.newm.shared.login.repository.LogInRepository import io.newm.shared.models.Song import io.newm.shared.repositories.CardanoWalletRepository import io.newm.shared.repositories.NFTTrack import io.newm.shared.repositories.testdata.MockSongs -import io.newm.shared.services.LedgerAssetMetadata -import io.newm.shared.services.UserAPI import kotlinx.coroutines.flow.Flow -import org.koin.core.component.inject interface WalletNFTSongsUseCase { fun getAllWalletNFTSongs(xPub: String): Flow> diff --git a/shared/src/commonMain/kotlin/shared/NotificationCenter.kt b/shared/src/commonMain/kotlin/shared/NotificationCenter.kt new file mode 100644 index 00000000..ad9f6868 --- /dev/null +++ b/shared/src/commonMain/kotlin/shared/NotificationCenter.kt @@ -0,0 +1,7 @@ +package shared + +expect fun postNotification(name: String) + +object Notification { + const val loginStateChanged = "login state changed" +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/shared/SecureStorage.kt b/shared/src/commonMain/kotlin/shared/SecureStorage.kt deleted file mode 100644 index c0bf984f..00000000 --- a/shared/src/commonMain/kotlin/shared/SecureStorage.kt +++ /dev/null @@ -1,7 +0,0 @@ -package shared - -expect class SecureStorage { - fun store(key: String, value: String) - fun retrieve(key: String): String? - fun remove(key: String) -} \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/shared/NotificationCenter.kt b/shared/src/iosMain/kotlin/shared/NotificationCenter.kt new file mode 100644 index 00000000..d4bc89e4 --- /dev/null +++ b/shared/src/iosMain/kotlin/shared/NotificationCenter.kt @@ -0,0 +1,7 @@ +package shared + +import platform.Foundation.NSNotificationCenter + +actual fun postNotification(name: String) { + NSNotificationCenter.defaultCenter.postNotificationName(name, null) +} diff --git a/shared/src/iosMain/kotlin/shared/SecureStorage.kt b/shared/src/iosMain/kotlin/shared/SecureStorage.kt deleted file mode 100644 index ffd4d8af..00000000 --- a/shared/src/iosMain/kotlin/shared/SecureStorage.kt +++ /dev/null @@ -1,63 +0,0 @@ -package shared - -import platform.Foundation.NSData -import platform.Foundation.NSString -import platform.Foundation.create -import platform.Foundation.kCFBooleanTrue -import platform.Security.SecItemAdd -import platform.Security.SecItemCopyMatching -import platform.Security.SecItemDelete -import platform.Security.kSecAttrAccount -import platform.Security.kSecAttrService -import platform.Security.kSecClass -import platform.Security.kSecClassGenericPassword -import platform.Security.kSecMatchLimit -import platform.Security.kSecMatchLimitOne -import platform.Security.kSecReturnData -import platform.Security.kSecValueData -import platform.darwin.errSecSuccess - -actual class SecureStorage(private val account: String) { - actual fun store(key: String, value: String) { - val data = value.encodeToByteArray().toNSData() - val query = dictOf( - kSecClass to kSecClassGenericPassword, - kSecAttrAccount to key, - kSecValueData to data - ) - - SecItemDelete(query) - SecItemAdd(query, null) - } - - actual fun retrieve(key: String): String? { - val query = dictOf( - kSecClass to kSecClassGenericPassword, - kSecAttrAccount to key, - kSecReturnData to kCFBooleanTrue, - kSecMatchLimit to kSecMatchLimitOne - ) - - memScoped { - val dataRef = alloc>() - val status = SecItemCopyMatching(query, dataRef.ptr.reinterpret()) - - if (status == errSecSuccess) { - dataRef.value?.let { data -> - return data.toByteArray().decodeToString() - } - } - } - - return null - } - - actual fun remove(key: String) { - val query = dictOf( - kSecClass to kSecClassGenericPassword, - kSecAttrAccount to key - ) - - SecItemDelete(query) - } -} diff --git a/shared/src/iosMain/kotlin/shared/actual.kt b/shared/src/iosMain/kotlin/shared/actual.kt index 71afad5c..96e998ce 100644 --- a/shared/src/iosMain/kotlin/shared/actual.kt +++ b/shared/src/iosMain/kotlin/shared/actual.kt @@ -1,9 +1,10 @@ package shared +import com.liftric.kvault.KVault import com.squareup.sqldelight.drivers.native.NativeSqliteDriver -import io.ktor.client.engine.darwin.* -import io.newm.shared.db.cache.NewmDatabase +import io.ktor.client.engine.darwin.Darwin import io.newm.shared.db.NewmDatabaseWrapper +import io.newm.shared.db.cache.NewmDatabase import org.koin.dsl.module actual fun platformModule() = module { @@ -12,5 +13,5 @@ actual fun platformModule() = module { NewmDatabaseWrapper(NewmDatabase(driver)) } single { Darwin.create() } - single { SecureStorage("user-account") } + single { KVault() } } \ No newline at end of file