Skip to content

Commit

Permalink
Merge pull request #170 from tidal-music/alberto/testAuthApp-changes
Browse files Browse the repository at this point in the history
Fixed Auth Login Flow in AuthTestApp
  • Loading branch information
asendra authored Nov 21, 2024
2 parents 391640c + d1d4435 commit 8dbcd01
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 95 deletions.
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ let package = Package(
name: "TidalAPI",
dependencies: [
.AnyCodable,
.auth
.auth,
.common,
],
plugins: [
.plugin(name: "SwiftLint", package: "SwiftLintPlugin"),
Expand Down
34 changes: 20 additions & 14 deletions TestApps/Auth/AuthTestApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,27 @@
objects = {

/* Begin PBXBuildFile section */
C625802E2CEF3AE700D2B293 /* PresentationContextProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C625802D2CEF3AE700D2B293 /* PresentationContextProvider.swift */; };
E16DACE62B97196A006C66C9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E16DACE52B97196A006C66C9 /* Assets.xcassets */; };
E16DACE92B97196A006C66C9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E16DACE82B97196A006C66C9 /* Preview Assets.xcassets */; };
E16DACF42B971AD9006C66C9 /* AuthTestAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16DACF02B971AD9006C66C9 /* AuthTestAppApp.swift */; };
E16DACF52B971AD9006C66C9 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16DACF12B971AD9006C66C9 /* HomeView.swift */; };
E16DACF62B971AD9006C66C9 /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16DACF22B971AD9006C66C9 /* AuthViewModel.swift */; };
E16DACFE2B971B7F006C66C9 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16DACFD2B971B7F006C66C9 /* AuthView.swift */; };
E16DAD012B971BE1006C66C9 /* SafariWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16DAD002B971BE1006C66C9 /* SafariWebView.swift */; };
E1D552F32B97303F00C26F97 /* Auth in Frameworks */ = {isa = PBXBuildFile; productRef = E1D552F22B97303F00C26F97 /* Auth */; };
E1D552F92B9730F300C26F97 /* tidal-sdk-ios in Resources */ = {isa = PBXBuildFile; fileRef = E1D552F82B9730ED00C26F97 /* tidal-sdk-ios */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
C625802C2CEE301100D2B293 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
C625802D2CEF3AE700D2B293 /* PresentationContextProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentationContextProvider.swift; sourceTree = "<group>"; };
C6D0C3312BF348CA005442B2 /* AuthTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AuthTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
E16DACE52B97196A006C66C9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
E16DACE82B97196A006C66C9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
E16DACF02B971AD9006C66C9 /* AuthTestAppApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AuthTestAppApp.swift; path = AuthTestApp/AuthTestAppApp.swift; sourceTree = SOURCE_ROOT; };
E16DACF12B971AD9006C66C9 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HomeView.swift; path = AuthTestApp/HomeView.swift; sourceTree = SOURCE_ROOT; };
E16DACF22B971AD9006C66C9 /* AuthViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AuthViewModel.swift; path = AuthTestApp/AuthViewModel.swift; sourceTree = SOURCE_ROOT; };
E16DACFD2B971B7F006C66C9 /* AuthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = "<group>"; };
E16DAD002B971BE1006C66C9 /* SafariWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebView.swift; sourceTree = "<group>"; };
E1D552F82B9730ED00C26F97 /* tidal-sdk-ios */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "tidal-sdk-ios"; path = ../..; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand All @@ -42,23 +43,32 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
C657E4142CE2251B00D16710 /* Frameworks */ = {
isa = PBXGroup;
children = (
);
name = Frameworks;
sourceTree = "<group>";
};
E16DACD52B971969006C66C9 = {
isa = PBXGroup;
children = (
E1D552F82B9730ED00C26F97 /* tidal-sdk-ios */,
E16DACE02B971969006C66C9 /* AuthTestApp */,
C6D0C3312BF348CA005442B2 /* AuthTestApp.app */,
C657E4142CE2251B00D16710 /* Frameworks */,
);
sourceTree = "<group>";
};
E16DACE02B971969006C66C9 /* AuthTestApp */ = {
isa = PBXGroup;
children = (
E16DACFF2B971BD3006C66C9 /* Utils */,
E16DACFD2B971B7F006C66C9 /* AuthView.swift */,
C625802C2CEE301100D2B293 /* Info.plist */,
E16DACF02B971AD9006C66C9 /* AuthTestAppApp.swift */,
E16DACF22B971AD9006C66C9 /* AuthViewModel.swift */,
E16DACF12B971AD9006C66C9 /* HomeView.swift */,
E16DACFD2B971B7F006C66C9 /* AuthView.swift */,
E16DACF22B971AD9006C66C9 /* AuthViewModel.swift */,
C625802D2CEF3AE700D2B293 /* PresentationContextProvider.swift */,
E16DACE52B97196A006C66C9 /* Assets.xcassets */,
E16DACE72B97196A006C66C9 /* Preview Content */,
);
Expand All @@ -73,14 +83,6 @@
path = "Preview Content";
sourceTree = "<group>";
};
E16DACFF2B971BD3006C66C9 /* Utils */ = {
isa = PBXGroup;
children = (
E16DAD002B971BE1006C66C9 /* SafariWebView.swift */,
);
path = Utils;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
Expand Down Expand Up @@ -157,8 +159,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
C625802E2CEF3AE700D2B293 /* PresentationContextProvider.swift in Sources */,
E16DACF62B971AD9006C66C9 /* AuthViewModel.swift in Sources */,
E16DAD012B971BE1006C66C9 /* SafariWebView.swift in Sources */,
E16DACF52B971AD9006C66C9 /* HomeView.swift in Sources */,
E16DACFE2B971B7F006C66C9 /* AuthView.swift in Sources */,
E16DACF42B971AD9006C66C9 /* AuthTestAppApp.swift in Sources */,
Expand Down Expand Up @@ -298,11 +300,13 @@
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AuthTestApp/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -328,11 +332,13 @@
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = AuthTestApp/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
{
"pins" : [
{
"identity" : "anycodable",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Flight-School/AnyCodable",
"state" : {
"revision" : "862808b2070cd908cb04f9aafe7de83d35f81b05",
"version" : "0.6.7"
}
},
{
"identity" : "grdb.swift",
"kind" : "remoteSourceControl",
Expand Down Expand Up @@ -27,6 +36,15 @@
"version" : "4.2.2"
}
},
{
"identity" : "swift-log",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git",
"state" : {
"revision" : "9cb486020ebf03bfa5b5df985387a14a98744537",
"version" : "1.6.1"
}
},
{
"identity" : "swiftlintplugin",
"kind" : "remoteSourceControl",
Expand Down
2 changes: 1 addition & 1 deletion TestApps/Auth/AuthTestApp/AuthTestAppApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import SwiftUI
struct AuthTestAppApp: App {
var body: some Scene {
WindowGroup {
NavigationStack {
NavigationView {
HomeView()
}
.environment(\.colorScheme, .dark)
Expand Down
11 changes: 0 additions & 11 deletions TestApps/Auth/AuthTestApp/AuthView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Auth
import SwiftUI

struct AuthView: View {
@State private var presentLoginScreen = false
@StateObject var viewModel: AuthViewModel = AuthViewModel()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

Expand Down Expand Up @@ -60,17 +59,7 @@ struct AuthView: View {

if !viewModel.isLoggedIn, !viewModel.isDeviceLoginEnabled {
Button("Login") {
viewModel.errorMessage = ""
viewModel.initializeLogin()
presentLoginScreen = true
}
.sheet(isPresented: $presentLoginScreen) {
if let url = viewModel.loginUrl {
SafariWebView(url: url, redicrectUrl: AuthViewModel.redirectUri) { url in
viewModel.finalizeLogin(url)
presentLoginScreen = false
}
}
}
}

Expand Down
111 changes: 89 additions & 22 deletions TestApps/Auth/AuthTestApp/AuthViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import Auth
import AuthenticationServices
import Foundation

// MARK: - AuthViewModel

@MainActor
class AuthViewModel: ObservableObject {
class AuthViewModel: NSObject, ObservableObject {
private let CLIENT_UNIQUE_KEY = "ClientUniqueKey"
private let CLIENT_ID_DEVICE_LOGIN = "ClientID2"
private let CLIENT_ID_DEVICE_LOGIN = "ClientIDDeviceLogin"
private let CLIENT_ID = "ClientID"
static let redirectUri = "https://tidal.com/ios/login/auth"
private let CLIENT_SECRET = "ClientSecret"
private let customScheme = "testauthsdk"
private let redirectUri = "testauthsdk://auth-test"
private let scopes: Set<String> = []

@Published var isDeviceLoginEnabled: Bool = false {
didSet {
Expand All @@ -18,30 +24,39 @@ class AuthViewModel: ObservableObject {
@Published var isLoggedIn: Bool = false
@Published var errorMessage: String = ""
@Published var expiresIn: String = ""
private(set) var loginUrl: URL?
private var expiresAt: Date = Date()

private var authConfig: AuthConfig {
AuthConfig(
clientId: isDeviceLoginEnabled ? CLIENT_ID_DEVICE_LOGIN : CLIENT_ID,
clientUniqueKey: CLIENT_UNIQUE_KEY,
credentialsKey: "storage"
)
}
private var webAuthSession: ASWebAuthenticationSession?
private var contextProvider: ASWebAuthenticationPresentationContextProviding?

private var loginUrl: URL?
private var loginConfig: LoginConfig {
LoginConfig(customParams: [QueryParameter(key: "appMode", value: "iOS")])
}

var isDeviceLoginCodeExpired: Bool {
private var expiresAt: Date = Date()
private var isDeviceLoginCodeExpired: Bool {
Date() > expiresAt
}

var auth: TidalAuth {
private var auth: TidalAuth {
.shared
}

init() {
private var authConfig: AuthConfig {
AuthConfig(
clientId: isDeviceLoginEnabled ? CLIENT_ID_DEVICE_LOGIN : CLIENT_ID,
clientUniqueKey: CLIENT_UNIQUE_KEY,
credentialsKey: "auth-storage",
scopes: scopes
)
}

override init() {
super.init()
initAuth()
}

func initAuth() {
auth.config(config: authConfig)
isLoggedIn = auth.isUserLoggedIn
}
Expand All @@ -53,26 +68,31 @@ class AuthViewModel: ObservableObject {
errorMessage = ""
isLoggedIn = auth.isUserLoggedIn
} catch {
errorMessage = error.localizedDescription
errorMessage = "Logout error: " + error.localizedDescription
}
}

func initializeLogin() {
if let url = auth.initializeLogin(redirectUri: Self.redirectUri, loginConfig: loginConfig) {
loginUrl = url
} else {
guard let url = auth.initializeLogin(redirectUri: redirectUri, loginConfig: loginConfig) else {
loginUrl = nil
errorMessage = "Error: login URL can't be populated"
return
}
startAuthenticationFlow(with: url)
}

func finalizeLogin(_ url: String) {
Task {
defer {
contextProvider = nil
webAuthSession = nil
}

do {
try await auth.finalizeLogin(loginResponseUri: url)
isLoggedIn = auth.isUserLoggedIn
} catch {
errorMessage = error.localizedDescription
errorMessage = "Login error: " + error.localizedDescription
}
}
}
Expand All @@ -89,6 +109,8 @@ class AuthViewModel: ObservableObject {
} catch let error as TidalError {
if let message = error.message {
errorMessage = message
} else {
errorMessage = error.localizedDescription
}
} catch {
errorMessage = error.localizedDescription
Expand All @@ -102,7 +124,52 @@ class AuthViewModel: ObservableObject {
return
}

let duration = Duration.seconds(expiresAt.timeIntervalSinceNow)
expiresIn = duration.formatted(.time(pattern: .minuteSecond(padMinuteToLength: 2)))
if #available(iOS 16, *) {
let duration = Duration.seconds(expiresAt.timeIntervalSinceNow)
expiresIn = duration.formatted(.time(pattern: .minuteSecond(padMinuteToLength: 2)))
} else {
let timeInterval = expiresAt.timeIntervalSinceNow
let minutes = Int(timeInterval) / 60
let seconds = Int(timeInterval) % 60
expiresIn = String(format: "%02d:%02d", minutes, seconds)
}
}
}

private extension AuthViewModel {
private func startAuthenticationFlow(with loginURL: URL) {
errorMessage = ""
loginUrl = loginURL

let completionHandler: ASWebAuthenticationSession.CompletionHandler = { [weak self] callbackURL, error in
guard let self else {
return
}
if let error {
errorMessage = "Authentication failed: " + error.localizedDescription
} else if let callbackURL {
finalizeLogin(callbackURL.absoluteString)
}
}

if #available(iOS 17.4, *) {
webAuthSession = ASWebAuthenticationSession(
url: loginURL,
callback: .customScheme(customScheme),
completionHandler: completionHandler
)
} else {
webAuthSession = ASWebAuthenticationSession(
url: loginURL,
callbackURLScheme: customScheme,
completionHandler: completionHandler
)
}

// Provide the presentation context for iPad compatibility
contextProvider = PresentationContextProvider()
webAuthSession?.presentationContextProvider = contextProvider
webAuthSession?.prefersEphemeralWebBrowserSession = false
webAuthSession?.start()
}
}
19 changes: 19 additions & 0 deletions TestApps/Auth/AuthTestApp/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-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>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.tidal.AuthTestApp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>testauthsdk</string>
</array>
</dict>
</array>
</dict>
</plist>
8 changes: 8 additions & 0 deletions TestApps/Auth/AuthTestApp/PresentationContextProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import AuthenticationServices
import Foundation

class PresentationContextProvider: NSObject, ASWebAuthenticationPresentationContextProviding {
func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor {
ASPresentationAnchor()
}
}
Loading

0 comments on commit 8dbcd01

Please sign in to comment.