Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions flo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
objects = {

/* Begin PBXBuildFile section */
50C912892F5DD9280087EE61 /* AuthMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C912872F5DD9280087EE61 /* AuthMode.swift */; };
50C912912F5DD9990087EE61 /* IAPLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C9128A2F5DD9990087EE61 /* IAPLoginView.swift */; };
50C912A42F648A440087EE61 /* IAPWebAuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C912A32F648A440087EE61 /* IAPWebAuthView.swift */; };
50C912A62F648A490087EE61 /* IAPWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C912A52F648A490087EE61 /* IAPWebView.swift */; };
B02A003F2F36662C0024E8EC /* UIScreen+.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02A003E2F3666240024E8EC /* UIScreen+.swift */; };
B0AD2E712F4B037400577062 /* ArtistDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AD2E6E2F4B037400577062 /* ArtistDetailView.swift */; };
B0AD2E722F4B037400577062 /* ArtistsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0AD2E6F2F4B037400577062 /* ArtistsView.swift */; };
Expand Down Expand Up @@ -147,6 +151,10 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
50C912872F5DD9280087EE61 /* AuthMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthMode.swift; sourceTree = "<group>"; };
50C9128A2F5DD9990087EE61 /* IAPLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPLoginView.swift; sourceTree = "<group>"; };
50C912A32F648A440087EE61 /* IAPWebAuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPWebAuthView.swift; sourceTree = "<group>"; };
50C912A52F648A490087EE61 /* IAPWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAPWebView.swift; sourceTree = "<group>"; };
B02A003E2F3666240024E8EC /* UIScreen+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScreen+.swift"; sourceTree = "<group>"; };
B0AD2E6E2F4B037400577062 /* ArtistDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistDetailView.swift; sourceTree = "<group>"; };
B0AD2E6F2F4B037400577062 /* ArtistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistsView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -320,13 +328,16 @@
C4289F4D2C1253EB00C3A4FD /* Utils */ = {
isa = PBXGroup;
children = (
50C912A52F648A490087EE61 /* IAPWebView.swift */,
50C912A32F648A440087EE61 /* IAPWebAuthView.swift */,
B02A003E2F3666240024E8EC /* UIScreen+.swift */,
C456D8F92F2FF33B002AAB8B /* LRCParser.swift */,
C4F0B0A12F3A111100ABC002 /* AirPlayRoutePicker.swift */,
C415F5592C11953000E3E1D2 /* Constants.swift */,
C415F5632C11AA8700E3E1D2 /* Fonts.swift */,
C49134522C15BE0C00CCF2EB /* Strings.swift */,
C4120FD82C15D58E00E712BE /* Errors.swift */,
50C9128A2F5DD9990087EE61 /* IAPLoginView.swift */,
);
path = Utils;
sourceTree = "<group>";
Expand Down Expand Up @@ -457,6 +468,7 @@
C4DFFA202D32E769003B9C4E /* DownloadViewModel.swift */,
C456D8FD2F300D37002AAB8B /* LyricsView.swift */,
C42B25662F44533D00E62008 /* Watch */,
50C912872F5DD9280087EE61 /* AuthMode.swift */,
);
path = flo;
sourceTree = "<group>";
Expand Down Expand Up @@ -633,6 +645,7 @@
C49495812C1C25E5006B4D1E /* ScanStatus.swift in Sources */,
C4289F482C12391300C3A4FD /* AlbumViewModel.swift in Sources */,
C42B25882F4456BF00E62008 /* WatchLibraryResponder.swift in Sources */,
50C912A62F648A490087EE61 /* IAPWebView.swift in Sources */,
C49495852C1C26D4006B4D1E /* ScanStatusService.swift in Sources */,
C47876042C2BFFF900184A33 /* SongView.swift in Sources */,
C446A6B72C08DE8800CC9787 /* UserAuth.swift in Sources */,
Expand All @@ -648,8 +661,10 @@
C4120FDD2C15E1C300E712BE /* Song.swift in Sources */,
C467AD532D3267D000644E68 /* Subsonic.swift in Sources */,
C47876022C2BF15900184A33 /* AlbumsView.swift in Sources */,
50C912A42F648A440087EE61 /* IAPWebAuthView.swift in Sources */,
C4824D272CE908DC003EAB52 /* SongsView.swift in Sources */,
C456D8FA2F2FF33E002AAB8B /* LRCParser.swift in Sources */,
50C912892F5DD9280087EE61 /* AuthMode.swift in Sources */,
C4F870CE2CEFCC5E00312F8A /* FloooService.swift in Sources */,
C4DFFA212D32E76E003B9C4E /* DownloadViewModel.swift in Sources */,
C4F1A0012F4A000100AAAAAA /* InAppPurchaseManager.swift in Sources */,
Expand Down Expand Up @@ -677,6 +692,7 @@
C4A4BF3D2C1455A100363290 /* FloatingPlayerView.swift in Sources */,
C415F54E2C11908100E3E1D2 /* AuthViewModel.swift in Sources */,
C42B25842F44551B00E62008 /* PlaybackCoordinator.swift in Sources */,
50C912912F5DD9990087EE61 /* IAPLoginView.swift in Sources */,
C4A4BF372C14442F00363290 /* DownloadsView.swift in Sources */,
C4100A692CE78B25001BC9BE /* PlaylistView.swift in Sources */,
C440228D2C09BE2E004EE9CD /* PlayerView.swift in Sources */,
Expand Down
25 changes: 25 additions & 0 deletions flo/AuthMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// AuthMode.swift
// flo
//
// Created by piekay on 08/03/26.
//

import Foundation

enum AuthMode: String, Codable {
case standard
case iap
}

struct IAPAuthInfo: Codable {
let jwtAssertion: String
let userEmail: String?
let userId: String?

init(jwtAssertion: String, userEmail: String? = nil, userId: String? = nil) {
self.jwtAssertion = jwtAssertion
self.userEmail = userEmail
self.userId = userId
}
}
78 changes: 77 additions & 1 deletion flo/AuthViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class AuthViewModel: ObservableObject {

@Published var isSubmitting: Bool = false
@Published var isLoggedIn: Bool = false

@Published var authMode: AuthMode = .standard
@Published var iapJwtAssertion: String = ""
@Published var useIAPAuth: Bool = false

static let shared = AuthViewModel()

Expand All @@ -49,6 +53,8 @@ class AuthViewModel: ObservableObject {

self.serverUrl = UserDefaultsManager.serverBaseURL
self.username = data.username

self.authMode = AuthService.shared.getAuthMode()

if UserDefaultsManager.saveLoginInfo {
do {
Expand All @@ -57,7 +63,11 @@ class AuthViewModel: ObservableObject {
print("Error loading password from Keychain: \(error)")
}

self.login()
if authMode == .iap, let iapInfo = AuthService.shared.getIAPAuthInfo() {
self.loginWithIAP(jwtAssertion: iapInfo.jwtAssertion)
} else {
self.login()
}
} else {

self.user = UserAuth(
Expand Down Expand Up @@ -123,11 +133,18 @@ class AuthViewModel: ObservableObject {
try KeychainManager.removeAuthCreds()

self.destroySavedPassword()

if authMode == .iap {
try? KeychainManager.removeIAPAuthInfo()
try? KeychainManager.removeAuthMode()
AuthService.shared.clearIAPAuthInfo()
}

UserDefaultsManager.removeObject(key: UserDefaultsKeys.serverURL)

self.user = nil
self.isLoggedIn = false
self.authMode = .standard
} catch let error {
print("error>>>>> \(error)")
}
Expand Down Expand Up @@ -161,4 +178,63 @@ class AuthViewModel: ObservableObject {
print("Error saving data to Keychain: \(error)")
}
}

func loginWithIAP(jwtAssertion: String? = nil) {
isSubmitting = true

let jwt = jwtAssertion ?? self.iapJwtAssertion

guard !jwt.isEmpty else {
DispatchQueue.main.async {
self.isSubmitting = false
self.alertMessage = "JWT assertion is required for IAP authentication"
self.showAlert = true
}
return
}

AuthService.shared.loginWithIAP(serverUrl: serverUrl, jwtAssertion: jwt) { result in
switch result {
case .success(let data):
self.persistAuthData(data)

self.authMode = .iap

if UserDefaultsManager.saveLoginInfo {
self.destroySavedPassword()
}

DispatchQueue.main.async {
self.isSubmitting = false
self.isLoggedIn = true
self.iapJwtAssertion = ""
self.serverUrl = ""
}

case .failure(let error):
DispatchQueue.main.async {
self.isSubmitting = false

switch error {
case .server(let message):
self.alertMessage = message

case .unknown:
self.alertMessage = "Unknown error occurred during IAP authentication"
}

self.showAlert = true
}
}
}
}

func toggleAuthMode() {
useIAPAuth.toggle()
}

func isUsingIAPAuth() -> Bool {
return authMode == .iap
}
}

4 changes: 2 additions & 2 deletions flo/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIAppFonts</key>
<array>
<string>PlusJakartaSans-VariableFont_wght.ttf</string>
Expand Down
51 changes: 51 additions & 0 deletions flo/LoginView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import SwiftUI
struct Login: View {
@ObservedObject var viewModel: AuthViewModel
@Binding var showLoginSheet: Bool

@State private var showIAPLogin = false

var isSubmitButtonDisabled: Bool {
viewModel.serverUrl.isEmpty || viewModel.username.isEmpty || viewModel.password.isEmpty
Expand All @@ -31,6 +33,15 @@ struct Login: View {
dismissButton: .default(Text("OK"))
)
}
.sheet(isPresented: $showIAPLogin) {
IAPLoginView(authViewModel: viewModel)
}
.onChange(of: viewModel.isLoggedIn) { isLoggedIn in
if isLoggedIn {
showLoginSheet = false
showIAPLogin = false
}
}
.background(Color(.systemBackground))
.foregroundColor(.accent)
}
Expand Down Expand Up @@ -90,6 +101,7 @@ struct Login: View {
formField(title: "Username", text: $viewModel.username, placeholder: "sigma")
secureFormField(title: "Password", text: $viewModel.password, placeholder: "*************")
submitButton
iapLoginButton
}
.padding(.bottom, 30)
.padding(.horizontal, 10)
Expand Down Expand Up @@ -154,6 +166,45 @@ struct Login: View {
.disabled(isSubmitButtonDisabled)
}
}

private var iapLoginButton: some View {
VStack(spacing: 12) {
HStack {
VStack { Divider() }
Text("OR")
.customFont(.caption1)
.foregroundColor(.secondary)
.padding(.horizontal, 8)
VStack { Divider() }
}
.padding(.horizontal, 15)
.padding(.vertical, 10)

Button(action: { showIAPLogin = true }) {
HStack {
Image(systemName: "lock.shield.fill")
.font(.system(size: 16))
Text("Login with IAP")
.fontWeight(.semibold)
.customFont(.headline)
}
.foregroundColor(Color("PlayerColor"))
.padding()
.frame(maxWidth: .infinity)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color("PlayerColor"), lineWidth: 2)
)
}
.padding(.horizontal, 15)

Text("Use this if your server is behind OAuth2-Proxy or Identity-Aware Proxy")
.customFont(.caption1)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 20)
}
}
}

struct LoginView_Previews: PreviewProvider {
Expand Down
5 changes: 4 additions & 1 deletion flo/Navigation/PreferencesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -378,9 +378,12 @@ struct PreferencesView: View {
}
}
))
.disabled(authViewModel.authMode == .iap)

Text(
"flo will store your server URL, username, and password in the Keychain with no biometric protection. If you enable this, flo will try to 'refresh' the auth token—by logging you in automatically—every time you open flo so you'll never log out unless you do it explicitly (it will also reset this option)"
authViewModel.authMode == .iap
? "This option is not available when using OAuth."
: "flo will store your server URL, username, and password in the Keychain with no biometric protection. If you enable this, flo will try to 'refresh' the auth token—by logging you in automatically—every time you open flo so you'll never log out unless you do it explicitly (it will also reset this option). Logging in via OAuth will reset this option."
).font(.caption).foregroundColor(.gray)
}
.sheet(isPresented: shouldShowLoginSheet) {
Expand Down
Loading