Skip to content

Commit

Permalink
use user cookie login method
Browse files Browse the repository at this point in the history
  • Loading branch information
TheMoonThatRises committed Dec 5, 2024
1 parent fcb73b6 commit 6ebb7a6
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 143 deletions.
2 changes: 0 additions & 2 deletions Spwifiy/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
</array>
</dict>
</array>
<key>SpotifyClientId</key>
<string>72ef80b9d2aa4dd580f8a68f0c92334c</string>
<key>UIAppFonts</key>
<array>
<string>Satoshi.ttf</string>
Expand Down
8 changes: 4 additions & 4 deletions Spwifiy/Localizations/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,11 @@
"Artists" : {

},
"Attempting to authorize..." : {
"attempting authorization" : {

},
"By" : {

},
"Click the URL below if an authorization window does not appear" : {

},
"Discover" : {
"extractionState" : "manual"
Expand Down Expand Up @@ -70,6 +67,9 @@
},
"Liked songs" : {

},
"Login with Spotify" : {

},
"My Library" : {
"extractionState" : "manual"
Expand Down
35 changes: 35 additions & 0 deletions Spwifiy/Models/SpotifyAuthCookie.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// SpotifyAuthCookie.swift
// Spwifiy
//
// Created by Peter Duanmu on 12/4/24.
//

import Foundation

struct SpotifyAuthCookie: Codable {
let name: String
let value: String
let expiresDate: Date
let domain: String

var httpCookie: HTTPCookie? {
let properties: [HTTPCookiePropertyKey: Any] = [
.domain: domain,
.path: "/",
.name: name,
.value: value,
.secure: true,
.expires: expiresDate
]

return HTTPCookie(properties: properties)
}

init(cookie: HTTPCookie) {
self.name = cookie.name
self.value = cookie.value
self.expiresDate = cookie.expiresDate ?? Date.now
self.domain = cookie.domain
}
}
13 changes: 13 additions & 0 deletions Spwifiy/Models/SpotifyAuthResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// SpotifyAuthResponse.swift
// Spwifiy
//
// Created by Peter Duanmu on 12/4/24.
//

struct SpotifyAuthResponse: Decodable {
let clientId: String
let accessToken: String
let accessTokenExpirationTimestampMs: Double
let isAnonymous: Bool
}
46 changes: 13 additions & 33 deletions Spwifiy/SpwifiyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,51 +21,31 @@ struct SpwifiyApp: App {

@StateObject var spotifyCache: SpotifyCache = SpotifyCache()

@State var showAuthLoading: Bool = false
@State var showErrorMessage: Bool = false

@State var errorMessage: String = "" {
didSet {
showErrorMessage = !errorMessage.isEmpty
}
}

var body: some Scene {
WindowGroup {
Group {
if spotifyViewModel.isAuthorized {
switch mainViewModel.authStatus {
case .success:
MainView(spotifyViewModel: spotifyViewModel,
spotifyDataViewModel: spotifyDataViewModel,
mainViewModel: mainViewModel,
spotifyCache: spotifyCache)
.onAppear {
showAuthLoading = false
mainViewModel.currentView = .home
}
} else {
LoginView(spotifyViewModel: spotifyViewModel)
}
}
.handlesExternalEvents(preferring: ["{path of URL?}"], allowing: ["*"])
.onOpenURL { url in
Task { @MainActor in
if url.absoluteString.contains(SpotifyViewModel.loginCallback) {
do {
showAuthLoading = true

try await spotifyViewModel.spotifyRequestAccess(redirectURL: url)
} catch {
errorMessage = error.localizedDescription
case .inProcess, .failed:
LoginView(authStatus: $mainViewModel.authStatus)
case .cookieSet:
Text("attempting authorization")
.font(.satoshiBlack(24))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.task(priority: .utility) {
await spotifyViewModel.attemptSpotifyAuthToken()
}

showAuthLoading = false
}
}
}
.toast(isPresenting: $showAuthLoading) {
AlertToast(displayMode: .alert, type: .loading)
}
.toast(isPresenting: $showErrorMessage) {
AlertToast(displayMode: .alert, type: .error(.red), title: errorMessage)
.onChange(of: spotifyViewModel.isAuthorized) { newValue in
mainViewModel.authStatus = newValue == .valid ? .success : .failed
}
.frame(minWidth: 950, minHeight: 550)
.background(.bgMain)
Expand Down
16 changes: 14 additions & 2 deletions Spwifiy/Utils/API/APIRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,16 @@ class APIRequest {
self.noCacheSession.configuration.urlCache = nil
}

public func request(url: URL, noCache: Bool = false, success: @escaping (Data?) -> Void) {
(noCache ? noCacheSession : session).dataTask(with: URLRequest(url: url)) { data, _, error in
public func setCookie(cookie: HTTPCookie, noCache: Bool = false) {
(noCache ? noCacheSession : session).configuration.httpCookieStorage?.setCookie(cookie)
}

public func removeCookie(cookie: HTTPCookie, noCache: Bool = false) {
(noCache ? noCacheSession : session).configuration.httpCookieStorage?.deleteCookie(cookie)
}

public func request(request: URLRequest, noCache: Bool = false, success: @escaping (Data?) -> Void) {
(noCache ? noCacheSession : session).dataTask(with: request) { data, _, error in
if error == nil, let data = data {
success(data)
} else {
Expand All @@ -42,6 +50,10 @@ class APIRequest {
.resume()
}

public func request(url: URL, noCache: Bool = false, success: @escaping (Data?) -> Void) {
request(request: URLRequest(url: url), noCache: noCache, success: success)
}

public func request(urlString: String, noCache: Bool = false, success: @escaping (Data?) -> Void) {
guard let url = URL(string: urlString) else {
return success(nil)
Expand Down
101 changes: 101 additions & 0 deletions Spwifiy/Utils/API/SpotifyCustomLogin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// SpotifyCustomLogin.swift
// Spwifiy
//
// Created by Peter Duanmu on 12/4/24.
//

import SwiftUI
import WebKit
import KeychainAccess

class SpotifyAuthManager: NSObject, WKHTTPCookieStoreObserver {

public enum AuthStatus {
case success, failed, inProcess, cookieSet
}

public static let spDcCookieKey = "sp_dc_cookie"
public static let spTCookieKey = "sp_t_cookie"

@Binding var authStatus: AuthStatus

let keychain: Keychain
let webStore: WKWebsiteDataStore

init(webStore: WKWebsiteDataStore, authStatus: Binding<AuthStatus>) {
self._authStatus = authStatus
self.keychain = Keychain(service: SpwifiyApp.service)
self.webStore = webStore

super.init()

self.webStore.httpCookieStore.add(self)
}

func cookiesDidChange(in cookieStore: WKHTTPCookieStore) {
cookieStore.getAllCookies { cookies in
if let spDcCookie = cookies.filter({ $0.name == "sp_dc" }).first,
let spTCookie = cookies.filter({ $0.name == "sp_t" }).first {
let encoder = JSONEncoder()

do {
self.keychain[
data: SpotifyAuthManager.spDcCookieKey
] = try encoder.encode(SpotifyAuthCookie(cookie: spDcCookie))

self.keychain[
data: SpotifyAuthManager.spTCookieKey
] = try encoder.encode(SpotifyAuthCookie(cookie: spTCookie))

self.authStatus = .cookieSet
} catch {
print("unable to store cookie data in keychain: \(error)")

self.authStatus = .failed
}

cookies.forEach {
self.webStore.httpCookieStore.delete($0)
}

self.webStore.httpCookieStore.remove(self)
}
}
}

}

struct SpotifyWebView: NSViewRepresentable {

let webStore: WKWebsiteDataStore

let spotifyAuthManager: SpotifyAuthManager
var webView: WKWebView

init(authStatus: Binding<SpotifyAuthManager.AuthStatus>) {
self.webStore = .default()

let config = WKWebViewConfiguration()
config.websiteDataStore = webStore
config.limitsNavigationsToAppBoundDomains = false

self.webView = WKWebView(frame: .zero, configuration: config)

self.spotifyAuthManager = SpotifyAuthManager(
webStore: self.webStore,
authStatus: authStatus
)

self.webView.load(URLRequest(url: URL(string: "https://accounts.spotify.com/login")!))
}

func makeNSView(context: Context) -> some NSView {
return webView
}

func updateNSView(_ nsView: NSViewType, context: Context) {

}

}
2 changes: 2 additions & 0 deletions Spwifiy/ViewModels/MainViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import SpotifyWebAPI

class MainViewModel: ObservableObject {

@Published var authStatus: SpotifyAuthManager.AuthStatus = .cookieSet

@Published var currentView: MainViewOptions = .home {
willSet {
withAnimation(.defaultAnimation) {
Expand Down
Loading

0 comments on commit 6ebb7a6

Please sign in to comment.