From b80bffa927057aad96dbf1a1bc87b1d4009128eb Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Wed, 25 Oct 2023 13:38:16 +0100 Subject: [PATCH 01/11] ASWebAuthenticationSession: remove @MainActor from async wrappers --- Sources/SoundCloud/Extension/ASWebAuthenticationSession.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SoundCloud/Extension/ASWebAuthenticationSession.swift b/Sources/SoundCloud/Extension/ASWebAuthenticationSession.swift index f86649d..d8e795e 100644 --- a/Sources/SoundCloud/Extension/ASWebAuthenticationSession.swift +++ b/Sources/SoundCloud/Extension/ASWebAuthenticationSession.swift @@ -17,7 +17,7 @@ public extension ASWebAuthenticationSession { /// - context: Delegate object that specifies how to present web page. Defaults to UIApplication.shared.keyWindow /// - ephemeralSession: ๐Ÿชโ“ /// - Returns: Authorization code from callback URL - @MainActor static func getAuthCode( + static func getAuthCode( from url: String, with redirectURI: String, context: ASWebAuthenticationPresentationContextProviding = ApplicationWindowContextProvider(), @@ -60,7 +60,7 @@ public extension ASWebAuthenticationSession { /// - with: URI for OAuth web page to use to redirect back to your app. Should take the form "://" /// - ephemeralSession: ๐Ÿชโ“ /// - Returns: Authorization code from callback URL - @MainActor static func getAuthCode( + static func getAuthCode( from url: String, with redirectURI: String, ephemeralSession: Bool = false From 32f2859907431b0ba949f07be8a66e446d2ebce3 Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Wed, 25 Oct 2023 13:38:24 +0100 Subject: [PATCH 02/11] SoundCloud: put logging inside #if debug --- Sources/SoundCloud/SoundCloud.swift | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Sources/SoundCloud/SoundCloud.swift b/Sources/SoundCloud/SoundCloud.swift index 3930782..13c1e4f 100644 --- a/Sources/SoundCloud/SoundCloud.swift +++ b/Sources/SoundCloud/SoundCloud.swift @@ -16,10 +16,8 @@ final public class SoundCloud { public init(_ config: SoundCloud.Config) { self.config = config - decoder.keyDecodingStrategy = .convertFromSnakeCase - if let authTokens = try? tokenDAO.get() { - Logger.auth.info("๐Ÿ’พ Loaded saved access token: \(authTokens.accessToken, privacy: .private)") - } + decoder.keyDecodingStrategy = .convertFromSnakeCase // API keys use snake case + debugLogAuthToken() } } @@ -34,7 +32,7 @@ public extension SoundCloud { throw Error.userNotAuthorized } if savedAuthTokens.isExpired { - Logger.auth.warning("โฐ Access token expired at: \(savedAuthTokens.expiryDate!)") + debugLogAuthTokenExpired(savedAuthTokens.expiryDate!) do { try await refreshAuthTokens() } catch { @@ -182,7 +180,7 @@ private extension SoundCloud { func getNewAuthTokens(using authCode: String) async throws -> (TokenResponse) { let tokenResponse = try await get(.accessToken(authCode, config.clientId, config.clientSecret, config.redirectURI)) - Logger.auth.info("๐ŸŒŸ Received new access token: \(tokenResponse.accessToken, privacy: .private)") + debugLogNewAuthToken(tokenResponse.accessToken) return tokenResponse } @@ -190,9 +188,9 @@ private extension SoundCloud { guard let savedRefreshToken = try? tokenDAO.get().refreshToken else { throw Error.userNotAuthorized } - let newTokens = try await get(.refreshToken(savedRefreshToken, config.clientId, config.clientSecret, config.redirectURI)) - Logger.auth.info("โ™ป๏ธ Refreshed access token: \(newTokens.accessToken, privacy: .private)") - saveTokensWithCreationDate(newTokens) + let refreshedTokens = try await get(.refreshToken(savedRefreshToken, config.clientId, config.clientSecret, config.redirectURI)) + debugLogNewAuthToken(refreshedTokens.accessToken) + saveTokensWithCreationDate(refreshedTokens) } func saveTokensWithCreationDate(_ tokens: TokenResponse) { @@ -250,3 +248,26 @@ private extension SoundCloud { return request } } + +// MARK: - Debug logging +private extension SoundCloud { + func debugLogAuthToken() { + #if DEBUG + if let authToken = try? tokenDAO.get().accessToken { + Logger.auth.info("๐Ÿ’พ Persisted access token: \(authToken, privacy: .private)") + } + #endif + } + + func debugLogNewAuthToken(_ token: String) { + #if DEBUG + Logger.auth.info("๐ŸŒŸ Received new access token: \(token, privacy: .private)") + #endif + } + + func debugLogAuthTokenExpired(_ date: Date) { + #if DEBUG + Logger.auth.warning("โฐ Access token expired at: \(date)") + #endif + } +} From d794c60760a2e2f93e223f15edc6c2dc4ac59b44 Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Wed, 25 Oct 2023 13:39:31 +0100 Subject: [PATCH 03/11] SoundCloud: switch final <-> public --- Sources/SoundCloud/SoundCloud.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SoundCloud/SoundCloud.swift b/Sources/SoundCloud/SoundCloud.swift index 13c1e4f..9e9306b 100644 --- a/Sources/SoundCloud/SoundCloud.swift +++ b/Sources/SoundCloud/SoundCloud.swift @@ -8,7 +8,7 @@ import AuthenticationServices import OSLog -final public class SoundCloud { +public final class SoundCloud { private let config: SoundCloud.Config private let decoder = JSONDecoder() From b96015160e8920fa2b4db4efca49ecba696395ec Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Wed, 25 Oct 2023 14:50:56 +0100 Subject: [PATCH 04/11] SoundCloud: update comments, regroup methods in extensions --- Sources/SoundCloud/SoundCloud.swift | 110 +++++++++++++++------------- 1 file changed, 60 insertions(+), 50 deletions(-) diff --git a/Sources/SoundCloud/SoundCloud.swift b/Sources/SoundCloud/SoundCloud.swift index 9e9306b..3802da3 100644 --- a/Sources/SoundCloud/SoundCloud.swift +++ b/Sources/SoundCloud/SoundCloud.swift @@ -8,6 +8,13 @@ import AuthenticationServices import OSLog +/// Handles the logic for making authenticated requests to SoundCloud API. +/// +/// Use an instance of `SoundCloud` to allow users to login with their SoundCloud account and make authenticated +/// requests for streaming content and acessing track, artist, and playlist data from SoundCloud. +/// +/// - Important: OAuth tokens are stored in the `Keychain`. +/// - SeeAlso: Visit the [SoundCloud API Explorer](https://developers.soundcloud.com/docs/api/explorer/open-api#/) for more information. public final class SoundCloud { private let config: SoundCloud.Config @@ -21,13 +28,42 @@ public final class SoundCloud { } } -// MARK: - Auth ๐Ÿ” +// MARK: - ๐Ÿ‘€ public extension SoundCloud { - /// Dictionary with refreshed authorization token to be used as `URLRequest` header. + + // MARK: - Auth ๐Ÿ” + /// Performs the `OAuth` login flow and persists the resulting access tokens. + /// + /// This method does three things: + /// 1. Presents the SoundCloud login page inside a webview managed by ASWebAuthenticationSession to get the **authorization code** + /// 2. Exchanges the authorization code for **OAuth access tokens** specific to the SoundCloud user + /// 3. Persists the access tokens in the **Keychain** /// - /// **This getter will attempt to refresh the access token first if it is expired**, - /// throwing an error if it fails to refresh the token or doesn't find any persisted token. - var authHeader: [String : String] { get async throws { + /// - Throws: **`.cancelledLogin`** if logging in was cancelled manually by the user. + /// - Throws: **`.loggingIn`** if an error occurred while fetching the authorization code or authentication tokens. + func login() async throws { + do { + let authCode = try await getAuthorizationCode() + let newAuthTokens = try await getAuthenticationTokens(using: authCode) + saveTokensWithCreationDate(newAuthTokens) + } catch(ASWebAuthenticationSession.Error.cancelledLogin) { + throw Error.cancelledLogin + } catch { + throw Error.loggingIn + } + } + + /// Deletes the persisted access tokens. + func logout() { + try? tokenDAO.delete() + } + + /// Dictionary with valid auth token to be used as `URLRequest` header. + /// + /// - Throws: **`.userNotAuthorized`** if no access token exists. + /// - Throws: **`.refreshingExpiredAuthTokens`** if refreshing fails. + /// - Important: This **async** getter will attempt to refresh the access token first if it is expired. + var authenticatedHeader: [String : String] { get async throws { guard let savedAuthTokens = try? tokenDAO.get() else { throw Error.userNotAuthorized } @@ -42,26 +78,8 @@ public extension SoundCloud { let validAuthTokens = try! tokenDAO.get() return ["Authorization" : "Bearer " + (validAuthTokens.accessToken)] }} - - func login() async throws { - do { - let authCode = try await getAuthCode() - let newAuthTokens = try await getNewAuthTokens(using: authCode) - saveTokensWithCreationDate(newAuthTokens) - } catch(ASWebAuthenticationSession.Error.cancelledLogin) { - throw Error.cancelledLogin - } catch { - throw Error.loggingIn - } - } - - func logout() { - try? tokenDAO.delete() - } -} -// MARK: - My User ๐Ÿ’ -public extension SoundCloud { + // MARK: - My User ๐Ÿ•บ func getMyUser() async throws -> User { try await get(.myUser()) } @@ -85,10 +103,8 @@ public extension SoundCloud { func getMyLikedPlaylistsWithoutTracks() async throws -> [Playlist] { try await get(.myLikedPlaylists()) } -} -// MARK: - Tracks ๐Ÿ’ฟ -public extension SoundCloud { + // MARK: - Tracks ๐Ÿ’ฟ func getTracksForPlaylist(_ id: Int) async throws -> Page { try await get(.tracksForPlaylist(id)) } @@ -100,14 +116,8 @@ public extension SoundCloud { func getLikedTracksForUser(_ id: Int, _ limit: Int = 20) async throws -> Page { try await get(.likedTracksForUser(id, limit)) } - - func getStreamInfoForTrack(with id: Int) async throws -> StreamInfo { - try await get(.streamInfoForTrack(id)) - } -} -// MARK: - Search ๐Ÿ•ต๏ธ -public extension SoundCloud { + // MARK: - Search ๐Ÿ•ต๏ธ func searchTracks(_ query: String, _ limit: Int = 20) async throws -> Page { try await get(.searchTracks(query, limit)) } @@ -119,10 +129,10 @@ public extension SoundCloud { func searchUsers(_ query: String, _ limit: Int = 20) async throws -> Page { try await get(.searchUsers(query, limit)) } -} -// MARK: - Like + Follow ๐Ÿงก -public extension SoundCloud { + // MARK: - Like + Follow ๐Ÿงก + /// - Warning: The liked track may not be returned when calling `getMyLikedTracks()` since the API + /// appears to cache responses, consider keeping track of the liked tracks using a local array. func likeTrack(_ likedTrack: Track) async throws { try await get(.likeTrack(likedTrack.id)) } @@ -146,18 +156,22 @@ public extension SoundCloud { func unfollowUser(_ user: User) async throws { try await get(.unfollowUser(user.id)) } -} -// MARK: Miscellaneous โœจ -public extension SoundCloud { + // MARK: - Miscellaneous โœจ func pageOfItems(for href: String) async throws -> Page { try await get(.getNextPage(href)) } + + func getStreamInfoForTrack(with id: Int) async throws -> StreamInfo { + try await get(.streamInfoForTrack(id)) + } } -// MARK: - Private Auth ๐Ÿ™ˆ +// MARK: - ๐Ÿšซ๐Ÿ‘€ private extension SoundCloud { - func getAuthCode() async throws -> String { + + // MARK: - Auth ๐Ÿ” + func getAuthorizationCode() async throws -> String { let authorizeURL = config.apiURL + "connect" + "?client_id=\(config.clientId)" @@ -178,7 +192,7 @@ private extension SoundCloud { #endif } - func getNewAuthTokens(using authCode: String) async throws -> (TokenResponse) { + func getAuthenticationTokens(using authCode: String) async throws -> (TokenResponse) { let tokenResponse = try await get(.accessToken(authCode, config.clientId, config.clientSecret, config.redirectURI)) debugLogNewAuthToken(tokenResponse.accessToken) return tokenResponse @@ -198,10 +212,8 @@ private extension SoundCloud { tokensWithDate.expiryDate = tokens.expiresIn.dateWithSecondsAdded(to: Date()) try? tokenDAO.save(tokensWithDate) } -} -// MARK: - API request ๐ŸŒ -private extension SoundCloud { + // MARK: - API request ๐ŸŒ @discardableResult func get(_ request: Request) async throws -> T { try await fetchData(from: authorized(request)) @@ -243,14 +255,12 @@ private extension SoundCloud { request.httpMethod = scRequest.httpMethod if scRequest.shouldUseAuthHeader { - request.allHTTPHeaderFields = try await authHeader // Will refresh tokens if necessary + request.allHTTPHeaderFields = try await authenticatedHeader // Will refresh tokens if necessary } return request } -} -// MARK: - Debug logging -private extension SoundCloud { + // MARK: - Debug logging ๐Ÿ“ func debugLogAuthToken() { #if DEBUG if let authToken = try? tokenDAO.get().accessToken { From 3ab36b8afac58a9165154a28c6c0ff7dba469fe2 Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Wed, 25 Oct 2023 15:18:14 +0100 Subject: [PATCH 05/11] Tests: remove SC instance --- Tests/SoundCloudTests/SoundCloudTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SoundCloudTests/SoundCloudTests.swift b/Tests/SoundCloudTests/SoundCloudTests.swift index e923715..756ad20 100644 --- a/Tests/SoundCloudTests/SoundCloudTests.swift +++ b/Tests/SoundCloudTests/SoundCloudTests.swift @@ -6,6 +6,6 @@ final class SoundCloudTests: XCTestCase { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(SoundCloud().text, "Hello, World!") + } } From e771c19ff21350b47db4edd8c611e1980f932d3e Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Wed, 25 Oct 2023 16:27:48 +0100 Subject: [PATCH 06/11] Request: remove Foundation imports from request models --- Sources/SoundCloud/Models/Playlist.swift | 2 -- Sources/SoundCloud/Models/StatusCode.swift | 2 -- Sources/SoundCloud/Models/Test Models.swift | 8 -------- Sources/SoundCloud/Request.swift | 3 --- 4 files changed, 15 deletions(-) diff --git a/Sources/SoundCloud/Models/Playlist.swift b/Sources/SoundCloud/Models/Playlist.swift index 4977140..1214c30 100644 --- a/Sources/SoundCloud/Models/Playlist.swift +++ b/Sources/SoundCloud/Models/Playlist.swift @@ -5,8 +5,6 @@ // Created by Ryan Forsyth on 2023-10-03. // -import Foundation - public struct Playlist: Decodable, Identifiable, Equatable { public let id: Int public let genre: String diff --git a/Sources/SoundCloud/Models/StatusCode.swift b/Sources/SoundCloud/Models/StatusCode.swift index fe40666..e55cd79 100644 --- a/Sources/SoundCloud/Models/StatusCode.swift +++ b/Sources/SoundCloud/Models/StatusCode.swift @@ -5,8 +5,6 @@ // Created by Ryan Forsyth on 2023-09-07. // -import Foundation - public extension SoundCloud { enum StatusCode: Int { case success = 200 diff --git a/Sources/SoundCloud/Models/Test Models.swift b/Sources/SoundCloud/Models/Test Models.swift index f3c2378..7f66e35 100644 --- a/Sources/SoundCloud/Models/Test Models.swift +++ b/Sources/SoundCloud/Models/Test Models.swift @@ -5,9 +5,6 @@ // Created by Ryan Forsyth on 2023-10-03. // -import Foundation -import SwiftUI - public func testUser(_ id: Int = Int.random(in: 0..<1000)) -> User { User( avatarUrl: "https://i1.sndcdn.com/avatars-0DxRBnyCNCI3zL1X-oeoRyw-large.jpg", @@ -98,11 +95,6 @@ public func testTrack() -> Track { ) } -public func testTrackBinding() -> Binding { - var track = testTrack() - return Binding(get: { track }, set: { newTrack in track = newTrack }) -} - public var testDefaultLoadedPlaylists: [Int : Playlist] { var loadedPlaylists = [Int : Playlist]() let user = testUser() diff --git a/Sources/SoundCloud/Request.swift b/Sources/SoundCloud/Request.swift index 6218083..72bfdd4 100644 --- a/Sources/SoundCloud/Request.swift +++ b/Sources/SoundCloud/Request.swift @@ -4,9 +4,6 @@ // // Created by Ryan Forsyth on 2023-08-12. // -// https://developers.soundcloud.com/docs/api/explorer/open-api#/ - -import Foundation extension SoundCloud { From 79b8cdc78a7e5244cecf570eca17eecfae037d41 Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Wed, 25 Oct 2023 16:33:25 +0100 Subject: [PATCH 07/11] UserDefaultsDAO: update warning about behaviour --- Sources/SoundCloud/DAO/UserDefaultsDAO.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SoundCloud/DAO/UserDefaultsDAO.swift b/Sources/SoundCloud/DAO/UserDefaultsDAO.swift index 353c145..eed46ec 100644 --- a/Sources/SoundCloud/DAO/UserDefaultsDAO.swift +++ b/Sources/SoundCloud/DAO/UserDefaultsDAO.swift @@ -8,9 +8,9 @@ import Foundation /// Data access object used for persisting a `Codable` object to device's `UserDefaults`. /// -/// - Note: **Unexpected behaviour across app launches:** +/// - Warning: **Unexpected behaviour across app launches:** /// `UserDefaults` may not be properly synchronized after terminating app with Xcode. -/// **Terminate app via device** for expected synchronization behaviour. +/// **Terminate app via device** for expected read-write behaviour. public final class UserDefaultsDAO: DAO { public typealias DataType = T From 5e5d9ff7ac3769c1cf0f3b6b49d530e567b85902 Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Wed, 25 Oct 2023 16:44:21 +0100 Subject: [PATCH 08/11] SoundCloud: change tokenDAO type from concrete KeychainDAO to 'any DAO', allow differnt DAO to be injected --- Sources/SoundCloud/DAO/DAO.swift | 2 +- Sources/SoundCloud/DAO/KeychainDAO.swift | 2 +- Sources/SoundCloud/DAO/UserDefaultsDAO.swift | 5 ++--- Sources/SoundCloud/Models/TokenResponse.swift | 16 ++++++++-------- Sources/SoundCloud/SoundCloud.swift | 13 ++++++++++--- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/Sources/SoundCloud/DAO/DAO.swift b/Sources/SoundCloud/DAO/DAO.swift index 5c01531..a0161f0 100644 --- a/Sources/SoundCloud/DAO/DAO.swift +++ b/Sources/SoundCloud/DAO/DAO.swift @@ -8,7 +8,7 @@ /// Describes a data access object used for persisting an item of generic `Codable` type `DataType` /// - Parameters: /// - codingKey: key used to encode + decode persisted object -public protocol DAO: AnyObject { +public protocol DAO: AnyObject { associatedtype DataType: Codable var codingKey: String { get } diff --git a/Sources/SoundCloud/DAO/KeychainDAO.swift b/Sources/SoundCloud/DAO/KeychainDAO.swift index ed7d8ea..65ed301 100644 --- a/Sources/SoundCloud/DAO/KeychainDAO.swift +++ b/Sources/SoundCloud/DAO/KeychainDAO.swift @@ -16,7 +16,7 @@ public final class KeychainDAO: DAO { private let persistence = KeychainSwift() public var codingKey: String - init(_ codingKey: String) { + public init(_ codingKey: String) { self.codingKey = codingKey } diff --git a/Sources/SoundCloud/DAO/UserDefaultsDAO.swift b/Sources/SoundCloud/DAO/UserDefaultsDAO.swift index eed46ec..7a41bdb 100644 --- a/Sources/SoundCloud/DAO/UserDefaultsDAO.swift +++ b/Sources/SoundCloud/DAO/UserDefaultsDAO.swift @@ -8,9 +8,8 @@ import Foundation /// Data access object used for persisting a `Codable` object to device's `UserDefaults`. /// -/// - Warning: **Unexpected behaviour across app launches:** -/// `UserDefaults` may not be properly synchronized after terminating app with Xcode. -/// **Terminate app via device** for expected read-write behaviour. +/// - Warning: **Unexpected behaviour across app launches:** `UserDefaults` may not be properly synchronized +/// after terminating app with Xcode. **Terminate app via device** for expected read-write behaviour. public final class UserDefaultsDAO: DAO { public typealias DataType = T diff --git a/Sources/SoundCloud/Models/TokenResponse.swift b/Sources/SoundCloud/Models/TokenResponse.swift index cdb588f..6548b7d 100644 --- a/Sources/SoundCloud/Models/TokenResponse.swift +++ b/Sources/SoundCloud/Models/TokenResponse.swift @@ -7,14 +7,14 @@ import Foundation -internal struct TokenResponse: Codable { - let accessToken: String - let expiresIn: Int - let refreshToken: String - let scope: String - let tokenType: String - - var expiryDate: Date? = nil // Set when persisting object +public struct TokenResponse: Codable { + internal let accessToken: String + internal let expiresIn: Int + internal let refreshToken: String + internal let scope: String + internal let tokenType: String + + internal var expiryDate: Date? = nil // Set when persisting object } internal extension TokenResponse { diff --git a/Sources/SoundCloud/SoundCloud.swift b/Sources/SoundCloud/SoundCloud.swift index 3802da3..2d95747 100644 --- a/Sources/SoundCloud/SoundCloud.swift +++ b/Sources/SoundCloud/SoundCloud.swift @@ -10,19 +10,26 @@ import OSLog /// Handles the logic for making authenticated requests to SoundCloud API. /// +/// - parameter config: Contains parameters for interacting with SoundCloud API (base URL, client ID, secret, redirect URI) +/// - parameter tokenDAO: Data access object for persisting authentication tokens, defaults to **KeychainDAO** +/// /// Use an instance of `SoundCloud` to allow users to login with their SoundCloud account and make authenticated /// requests for streaming content and acessing track, artist, and playlist data from SoundCloud. /// -/// - Important: OAuth tokens are stored in the `Keychain`. +/// - Important: OAuth tokens are stored in the `Keychain` by default. /// - SeeAlso: Visit the [SoundCloud API Explorer](https://developers.soundcloud.com/docs/api/explorer/open-api#/) for more information. public final class SoundCloud { private let config: SoundCloud.Config + private let tokenDAO: any DAO private let decoder = JSONDecoder() - private let tokenDAO = KeychainDAO("OAuthTokenResponse") - public init(_ config: SoundCloud.Config) { + public init( + _ config: SoundCloud.Config, + _ tokenDAO: any DAO = KeychainDAO("OAuthTokenResponse") + ) { self.config = config + self.tokenDAO = tokenDAO decoder.keyDecodingStrategy = .convertFromSnakeCase // API keys use snake case debugLogAuthToken() } From 0d70c8225bd67afc00db3e6f8ce06c69bc5371b1 Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Wed, 25 Oct 2023 17:24:21 +0100 Subject: [PATCH 09/11] DAO: remove typealias from implementations --- Sources/SoundCloud/DAO/KeychainDAO.swift | 1 - Sources/SoundCloud/DAO/UserDefaultsDAO.swift | 1 - 2 files changed, 2 deletions(-) diff --git a/Sources/SoundCloud/DAO/KeychainDAO.swift b/Sources/SoundCloud/DAO/KeychainDAO.swift index 65ed301..6257238 100644 --- a/Sources/SoundCloud/DAO/KeychainDAO.swift +++ b/Sources/SoundCloud/DAO/KeychainDAO.swift @@ -9,7 +9,6 @@ import Foundation import KeychainSwift public final class KeychainDAO: DAO { - public typealias DataType = T private let encoder = JSONEncoder() private let decoder = JSONDecoder() diff --git a/Sources/SoundCloud/DAO/UserDefaultsDAO.swift b/Sources/SoundCloud/DAO/UserDefaultsDAO.swift index 7a41bdb..693f0a3 100644 --- a/Sources/SoundCloud/DAO/UserDefaultsDAO.swift +++ b/Sources/SoundCloud/DAO/UserDefaultsDAO.swift @@ -11,7 +11,6 @@ import Foundation /// - Warning: **Unexpected behaviour across app launches:** `UserDefaults` may not be properly synchronized /// after terminating app with Xcode. **Terminate app via device** for expected read-write behaviour. public final class UserDefaultsDAO: DAO { - public typealias DataType = T private let encoder = JSONEncoder() private let decoder = JSONDecoder() From 5947554656c136afdd4bbc8cb7fd7f562be0ff13 Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Wed, 1 Nov 2023 11:34:20 +0000 Subject: [PATCH 10/11] DAOError: move inside DAO file --- Sources/SoundCloud/DAO/DAO.swift | 6 ++++++ Sources/SoundCloud/DAO/DAOError.swift | 12 ------------ Sources/SoundCloud/SoundCloud.swift | 4 ++-- 3 files changed, 8 insertions(+), 14 deletions(-) delete mode 100644 Sources/SoundCloud/DAO/DAOError.swift diff --git a/Sources/SoundCloud/DAO/DAO.swift b/Sources/SoundCloud/DAO/DAO.swift index a0161f0..6341341 100644 --- a/Sources/SoundCloud/DAO/DAO.swift +++ b/Sources/SoundCloud/DAO/DAO.swift @@ -17,3 +17,9 @@ public protocol DAO: AnyObject { func save(_ value: DataType) throws func delete() throws } + +public enum DAOError: Error { + case noData + case decoding + case encoding +} diff --git a/Sources/SoundCloud/DAO/DAOError.swift b/Sources/SoundCloud/DAO/DAOError.swift deleted file mode 100644 index 7a9c2c9..0000000 --- a/Sources/SoundCloud/DAO/DAOError.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// DAOError.swift -// -// -// Created by Ryan Forsyth on 2023-10-20. -// - -public enum DAOError: Error { - case noData - case decoding - case encoding -} diff --git a/Sources/SoundCloud/SoundCloud.swift b/Sources/SoundCloud/SoundCloud.swift index 2d95747..fe744d4 100644 --- a/Sources/SoundCloud/SoundCloud.swift +++ b/Sources/SoundCloud/SoundCloud.swift @@ -39,7 +39,7 @@ public final class SoundCloud { public extension SoundCloud { // MARK: - Auth ๐Ÿ” - /// Performs the `OAuth` login flow and persists the resulting access tokens. + /// Performs the `OAuth` authentication flow and persists the resulting access tokens. /// /// This method does three things: /// 1. Presents the SoundCloud login page inside a webview managed by ASWebAuthenticationSession to get the **authorization code** @@ -69,7 +69,7 @@ public extension SoundCloud { /// /// - Throws: **`.userNotAuthorized`** if no access token exists. /// - Throws: **`.refreshingExpiredAuthTokens`** if refreshing fails. - /// - Important: This **async** getter will attempt to refresh the access token first if it is expired. + /// - Important: This **async** getter will first attempt to refresh the access token if it is expired. var authenticatedHeader: [String : String] { get async throws { guard let savedAuthTokens = try? tokenDAO.get() else { throw Error.userNotAuthorized From fe2e7da95a8d51dba20d952546b038fdda3d423a Mon Sep 17 00:00:00 2001 From: Ryan Forsyth Date: Sat, 11 Nov 2023 12:38:47 +0100 Subject: [PATCH 11/11] Page: add public init --- Sources/SoundCloud/Models/Page.swift | 5 +++++ Sources/SoundCloud/Models/Test Models.swift | 2 ++ Sources/SoundCloud/SoundCloud.swift | 6 +++--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/SoundCloud/Models/Page.swift b/Sources/SoundCloud/Models/Page.swift index 4bf9160..5985edd 100644 --- a/Sources/SoundCloud/Models/Page.swift +++ b/Sources/SoundCloud/Models/Page.swift @@ -8,6 +8,11 @@ public struct Page: Decodable { public var items: [ItemType] public var nextPage: String? + + public init(items: [ItemType], nextPage: String? = nil) { + self.items = items + self.nextPage = nextPage + } } extension Page { diff --git a/Sources/SoundCloud/Models/Test Models.swift b/Sources/SoundCloud/Models/Test Models.swift index 7f66e35..35cf33e 100644 --- a/Sources/SoundCloud/Models/Test Models.swift +++ b/Sources/SoundCloud/Models/Test Models.swift @@ -110,3 +110,5 @@ public let testNextProSubscription = User.Subscription(product: User.Subscriptio @MainActor public var testSC = SoundCloud(SoundCloud.Config(apiURL: "", clientId: "", clientSecret: "", redirectURI: "")) + +public let testStreamInfo = StreamInfo(httpMp3128Url: "", hlsMp3128Url: "") diff --git a/Sources/SoundCloud/SoundCloud.swift b/Sources/SoundCloud/SoundCloud.swift index fe744d4..34cb8b5 100644 --- a/Sources/SoundCloud/SoundCloud.swift +++ b/Sources/SoundCloud/SoundCloud.swift @@ -42,9 +42,9 @@ public extension SoundCloud { /// Performs the `OAuth` authentication flow and persists the resulting access tokens. /// /// This method does three things: - /// 1. Presents the SoundCloud login page inside a webview managed by ASWebAuthenticationSession to get the **authorization code** - /// 2. Exchanges the authorization code for **OAuth access tokens** specific to the SoundCloud user - /// 3. Persists the access tokens in the **Keychain** + /// 1. Presents the SoundCloud login page inside a webview managed by `ASWebAuthenticationSession` to get the **authorization code**. + /// 2. Exchanges the authorization code for **OAuth access tokens** specific to the SoundCloud user. + /// 3. Persists the **access tokens** using the data access object. /// /// - Throws: **`.cancelledLogin`** if logging in was cancelled manually by the user. /// - Throws: **`.loggingIn`** if an error occurred while fetching the authorization code or authentication tokens.