Skip to content

Commit 926fc05

Browse files
authored
feat: add Spotify authentication (#375)
* feat: add Spotify login to 3rd party login providers * feat: add optional parameters to Spotify login * test: add test cases for Spotify login * Update CHANGELOG.md * Update CHANGELOG.md * fix: Remove clientId from authentication data
1 parent b0bdf71 commit 926fc05

File tree

8 files changed

+1614
-0
lines changed

8 files changed

+1614
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.7.0...main)
66
* _Contributing to this repo? Add info about your change here to be included in the next release_
77

8+
__New features__
9+
- Add ParseSpotify authentication ([#375](https://github.com/parse-community/Parse-Swift/pull/375)), thanks to [Ulaş Sancak](https://github.com/rocxteady).
10+
811
__Fixes__
912
- Use select for ParseLiveQuery when fields are not present ([#376](https://github.com/parse-community/Parse-Swift/pull/376)), thanks to [Corey Baker](https://github.com/cbaker6).
1013

ParseSwift.xcodeproj/project.pbxproj

Lines changed: 62 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//
2+
// ParseSpotify+async.swift
3+
// ParseSwift
4+
//
5+
// Created by Ulaş Sancak on 06/20/22.
6+
// Copyright © 2022 Parse Community. All rights reserved.
7+
//
8+
9+
#if compiler(>=5.5.2) && canImport(_Concurrency)
10+
import Foundation
11+
12+
public extension ParseSpotify {
13+
// MARK: Async/Await
14+
15+
/**
16+
Login a `ParseUser` *asynchronously* using Spotify authentication.
17+
- parameter id: The **Spotify profile id** from **Spotify**.
18+
- parameter accessToken: Required **access_token** from **Spotify**.
19+
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
20+
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
21+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
22+
- returns: An instance of the logged in `ParseUser`.
23+
- throws: An error of type `ParseError`.
24+
*/
25+
func login(id: String,
26+
accessToken: String,
27+
expiresIn: Int? = nil,
28+
refreshToken: String? = nil,
29+
options: API.Options = []) async throws -> AuthenticatedUser {
30+
try await withCheckedThrowingContinuation { continuation in
31+
self.login(id: id,
32+
accessToken: accessToken,
33+
expiresIn: expiresIn,
34+
refreshToken: refreshToken,
35+
options: options,
36+
completion: continuation.resume)
37+
}
38+
}
39+
40+
/**
41+
Login a `ParseUser` *asynchronously* using Spotify authentication.
42+
- parameter authData: Dictionary containing key/values.
43+
- returns: An instance of the logged in `ParseUser`.
44+
- throws: An error of type `ParseError`.
45+
*/
46+
func login(authData: [String: String],
47+
options: API.Options = []) async throws -> AuthenticatedUser {
48+
try await withCheckedThrowingContinuation { continuation in
49+
self.login(authData: authData,
50+
options: options,
51+
completion: continuation.resume)
52+
}
53+
}
54+
}
55+
56+
public extension ParseSpotify {
57+
58+
/**
59+
Link the *current* `ParseUser` *asynchronously* using Spotify authentication.
60+
- parameter id: The **Spotify profile id** from **Spotify**.
61+
- parameter accessToken: Required **access_token** from **Spotify**.
62+
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
63+
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
64+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
65+
- returns: An instance of the logged in `ParseUser`.
66+
- throws: An error of type `ParseError`.
67+
*/
68+
func link(id: String,
69+
accessToken: String,
70+
expiresIn: Int? = nil,
71+
refreshToken: String? = nil,
72+
options: API.Options = []) async throws -> AuthenticatedUser {
73+
try await withCheckedThrowingContinuation { continuation in
74+
self.link(id: id,
75+
accessToken: accessToken,
76+
expiresIn: expiresIn,
77+
refreshToken: refreshToken,
78+
options: options,
79+
completion: continuation.resume)
80+
}
81+
}
82+
83+
/**
84+
Link the *current* `ParseUser` *asynchronously* using Spotify authentication.
85+
- parameter authData: Dictionary containing key/values.
86+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
87+
- returns: An instance of the logged in `ParseUser`.
88+
- throws: An error of type `ParseError`.
89+
*/
90+
func link(authData: [String: String],
91+
options: API.Options = []) async throws -> AuthenticatedUser {
92+
try await withCheckedThrowingContinuation { continuation in
93+
self.link(authData: authData,
94+
options: options,
95+
completion: continuation.resume)
96+
}
97+
}
98+
}
99+
#endif
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//
2+
// ParseSpotify+combine.swift
3+
// ParseSwift
4+
//
5+
// Created by Ulaş Sancak on 06/20/22.
6+
// Copyright © 2022 Parse Community. All rights reserved.
7+
//
8+
9+
#if canImport(Combine)
10+
import Foundation
11+
import Combine
12+
13+
public extension ParseSpotify {
14+
// MARK: Combine
15+
/**
16+
Login a `ParseUser` *asynchronously* using Spotify authentication. Publishes when complete.
17+
- parameter id: The **Spotify profile id** from **Spotify**.
18+
- parameter accessToken: Required **access_token** from **Spotify**.
19+
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
20+
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
21+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
22+
- returns: A publisher that eventually produces a single value and then finishes or fails.
23+
*/
24+
func loginPublisher(id: String,
25+
accessToken: String,
26+
expiresIn: Int? = nil,
27+
refreshToken: String? = nil,
28+
options: API.Options = []) -> Future<AuthenticatedUser, ParseError> {
29+
Future { promise in
30+
self.login(id: id,
31+
accessToken: accessToken,
32+
expiresIn: expiresIn,
33+
refreshToken: refreshToken,
34+
options: options,
35+
completion: promise)
36+
}
37+
}
38+
39+
/**
40+
Login a `ParseUser` *asynchronously* using Spotify authentication. Publishes when complete.
41+
- parameter authData: Dictionary containing key/values.
42+
- returns: A publisher that eventually produces a single value and then finishes or fails.
43+
*/
44+
func loginPublisher(authData: [String: String],
45+
options: API.Options = []) -> Future<AuthenticatedUser, ParseError> {
46+
Future { promise in
47+
self.login(authData: authData,
48+
options: options,
49+
completion: promise)
50+
}
51+
}
52+
}
53+
54+
public extension ParseSpotify {
55+
/**
56+
Link the *current* `ParseUser` *asynchronously* using Spotify authentication.
57+
Publishes when complete.
58+
- parameter id: The **Spotify profile id** from **Spotify**.
59+
- parameter accessToken: Required **access_token** from **Spotify**.
60+
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
61+
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
62+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
63+
- returns: A publisher that eventually produces a single value and then finishes or fails.
64+
*/
65+
func linkPublisher(id: String,
66+
accessToken: String,
67+
expiresIn: Int? = nil,
68+
refreshToken: String? = nil,
69+
options: API.Options = []) -> Future<AuthenticatedUser, ParseError> {
70+
Future { promise in
71+
self.link(id: id,
72+
accessToken: accessToken,
73+
expiresIn: expiresIn,
74+
refreshToken: refreshToken,
75+
options: options,
76+
completion: promise)
77+
}
78+
}
79+
80+
/**
81+
Link the *current* `ParseUser` *asynchronously* using Spotify authentication.
82+
Publishes when complete.
83+
- parameter authData: Dictionary containing key/values.
84+
- returns: A publisher that eventually produces a single value and then finishes or fails.
85+
*/
86+
func linkPublisher(authData: [String: String],
87+
options: API.Options = []) -> Future<AuthenticatedUser, ParseError> {
88+
Future { promise in
89+
self.link(authData: authData,
90+
options: options,
91+
completion: promise)
92+
}
93+
}
94+
}
95+
96+
#endif
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
//
2+
// ParseSpotify.swift
3+
// ParseSwift
4+
//
5+
// Created by Ulaş Sancak on 06/20/22.
6+
// Copyright © 2022 Parse Community. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
// swiftlint:disable line_length
12+
13+
/**
14+
Provides utility functions for working with Spotify User Authentication and `ParseUser`'s.
15+
Be sure your Parse Server is configured for [sign in with Spotify](https://docs.parseplatform.org/parse-server/guide/#spotify-authdata)
16+
For information on acquiring Spotify sign-in credentials to use with `ParseSpotify`, refer to [Spotify's Documentation](https://developer.spotify.com/documentation/general/guides/authorization/)
17+
*/
18+
public struct ParseSpotify<AuthenticatedUser: ParseUser>: ParseAuthentication {
19+
20+
/// Authentication keys required for Spotify authentication.
21+
enum AuthenticationKeys: String, Codable {
22+
case id
23+
case accessToken = "access_token"
24+
case expirationDate = "expiration_date"
25+
case refreshToken = "refresh_token"
26+
/// Properly makes an authData dictionary with the required keys.
27+
/// - parameter id: Required id for the user.
28+
/// - parameter accessToken: Required access token for Spotify.
29+
/// - parameter expiresIn: Optional expiration in seconds for Spotify.
30+
/// - parameter refreshToken: Optional refresh token for Spotify.
31+
/// - returns: authData dictionary.
32+
func makeDictionary(id: String,
33+
accessToken: String,
34+
expiresIn: Int? = nil,
35+
refreshToken: String? = nil) -> [String: String] {
36+
37+
var returnDictionary = [
38+
AuthenticationKeys.id.rawValue: id,
39+
AuthenticationKeys.accessToken.rawValue: accessToken
40+
]
41+
if let expiresIn = expiresIn,
42+
let expirationDate = Calendar.current.date(byAdding: .second,
43+
value: expiresIn,
44+
to: Date()) {
45+
let dateString = ParseCoding.dateFormatter.string(from: expirationDate)
46+
returnDictionary[AuthenticationKeys.expirationDate.rawValue] = dateString
47+
}
48+
if let refreshToken = refreshToken {
49+
returnDictionary[AuthenticationKeys.refreshToken.rawValue] = refreshToken
50+
}
51+
return returnDictionary
52+
}
53+
54+
/// Verifies all mandatory keys are in authData.
55+
/// - parameter authData: Dictionary containing key/values.
56+
/// - returns: **true** if all the mandatory keys are present, **false** otherwise.
57+
func verifyMandatoryKeys(authData: [String: String]) -> Bool {
58+
guard authData[AuthenticationKeys.id.rawValue] != nil,
59+
authData[AuthenticationKeys.accessToken.rawValue] != nil else {
60+
return false
61+
}
62+
return true
63+
}
64+
}
65+
66+
public static var __type: String { // swiftlint:disable:this identifier_name
67+
"spotify"
68+
}
69+
70+
public init() { }
71+
}
72+
73+
// MARK: Login
74+
public extension ParseSpotify {
75+
76+
/**
77+
Login a `ParseUser` *asynchronously* using Spotify authentication.
78+
- parameter id: The **Spotify profile id** from **Spotify**.
79+
- parameter accessToken: Required **access_token** from **Spotify**.
80+
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
81+
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
82+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
83+
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
84+
- parameter completion: The block to execute.
85+
*/
86+
func login(id: String,
87+
accessToken: String,
88+
expiresIn: Int? = nil,
89+
refreshToken: String? = nil,
90+
options: API.Options = [],
91+
callbackQueue: DispatchQueue = .main,
92+
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
93+
94+
let spotifyAuthData = AuthenticationKeys.id
95+
.makeDictionary(id: id,
96+
accessToken: accessToken,
97+
expiresIn: expiresIn,
98+
refreshToken: refreshToken)
99+
login(authData: spotifyAuthData,
100+
options: options,
101+
callbackQueue: callbackQueue,
102+
completion: completion)
103+
}
104+
105+
func login(authData: [String: String],
106+
options: API.Options = [],
107+
callbackQueue: DispatchQueue = .main,
108+
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
109+
guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData) else {
110+
callbackQueue.async {
111+
completion(.failure(.init(code: .unknownError,
112+
message: "Should have authData in consisting of keys \"id\" and \"accessToken\".")))
113+
}
114+
return
115+
}
116+
AuthenticatedUser.login(Self.__type,
117+
authData: authData,
118+
options: options,
119+
callbackQueue: callbackQueue,
120+
completion: completion)
121+
}
122+
}
123+
124+
// MARK: Link
125+
public extension ParseSpotify {
126+
127+
/**
128+
Link the *current* `ParseUser` *asynchronously* using Spotify authentication.
129+
- parameter id: The **Spotify profile id** from **Spotify**.
130+
- parameter accessToken: Required **access_token** from **Spotify**.
131+
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
132+
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
133+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
134+
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
135+
- parameter completion: The block to execute.
136+
*/
137+
func link(id: String,
138+
accessToken: String,
139+
expiresIn: Int? = nil,
140+
refreshToken: String? = nil,
141+
options: API.Options = [],
142+
callbackQueue: DispatchQueue = .main,
143+
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
144+
let spotifyAuthData = AuthenticationKeys.id
145+
.makeDictionary(id: id,
146+
accessToken: accessToken,
147+
expiresIn: expiresIn,
148+
refreshToken: refreshToken)
149+
link(authData: spotifyAuthData,
150+
options: options,
151+
callbackQueue: callbackQueue,
152+
completion: completion)
153+
}
154+
155+
func link(authData: [String: String],
156+
options: API.Options = [],
157+
callbackQueue: DispatchQueue = .main,
158+
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
159+
guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData) else {
160+
callbackQueue.async {
161+
completion(.failure(.init(code: .unknownError,
162+
message: "Should have authData in consisting of keys \"id\" and \"accessToken\".")))
163+
}
164+
return
165+
}
166+
AuthenticatedUser.link(Self.__type,
167+
authData: authData,
168+
options: options,
169+
callbackQueue: callbackQueue,
170+
completion: completion)
171+
}
172+
}
173+
174+
// MARK: 3rd Party Authentication - ParseSpotify
175+
public extension ParseUser {
176+
177+
/// A Spotify `ParseUser`.
178+
static var spotify: ParseSpotify<Self> {
179+
ParseSpotify<Self>()
180+
}
181+
182+
/// An Spotify `ParseUser`.
183+
var spotify: ParseSpotify<Self> {
184+
Self.spotify
185+
}
186+
}

0 commit comments

Comments
 (0)