Skip to content

Commit

Permalink
feat: add Spotify authentication (#375)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rocxteady authored Jul 9, 2022
1 parent b0bdf71 commit 926fc05
Show file tree
Hide file tree
Showing 8 changed files with 1,614 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.7.0...main)
* _Contributing to this repo? Add info about your change here to be included in the next release_

__New features__
- Add ParseSpotify authentication ([#375](https://github.com/parse-community/Parse-Swift/pull/375)), thanks to [Ulaş Sancak](https://github.com/rocxteady).

__Fixes__
- 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).

Expand Down
62 changes: 62 additions & 0 deletions ParseSwift.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
//
// ParseSpotify+async.swift
// ParseSwift
//
// Created by Ulaş Sancak on 06/20/22.
// Copyright © 2022 Parse Community. All rights reserved.
//

#if compiler(>=5.5.2) && canImport(_Concurrency)
import Foundation

public extension ParseSpotify {
// MARK: Async/Await

/**
Login a `ParseUser` *asynchronously* using Spotify authentication.
- parameter id: The **Spotify profile id** from **Spotify**.
- parameter accessToken: Required **access_token** from **Spotify**.
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`.
*/
func login(id: String,
accessToken: String,
expiresIn: Int? = nil,
refreshToken: String? = nil,
options: API.Options = []) async throws -> AuthenticatedUser {
try await withCheckedThrowingContinuation { continuation in
self.login(id: id,
accessToken: accessToken,
expiresIn: expiresIn,
refreshToken: refreshToken,
options: options,
completion: continuation.resume)
}
}

/**
Login a `ParseUser` *asynchronously* using Spotify authentication.
- parameter authData: Dictionary containing key/values.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`.
*/
func login(authData: [String: String],
options: API.Options = []) async throws -> AuthenticatedUser {
try await withCheckedThrowingContinuation { continuation in
self.login(authData: authData,
options: options,
completion: continuation.resume)
}
}
}

public extension ParseSpotify {

/**
Link the *current* `ParseUser` *asynchronously* using Spotify authentication.
- parameter id: The **Spotify profile id** from **Spotify**.
- parameter accessToken: Required **access_token** from **Spotify**.
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`.
*/
func link(id: String,
accessToken: String,
expiresIn: Int? = nil,
refreshToken: String? = nil,
options: API.Options = []) async throws -> AuthenticatedUser {
try await withCheckedThrowingContinuation { continuation in
self.link(id: id,
accessToken: accessToken,
expiresIn: expiresIn,
refreshToken: refreshToken,
options: options,
completion: continuation.resume)
}
}

/**
Link the *current* `ParseUser` *asynchronously* using Spotify authentication.
- parameter authData: Dictionary containing key/values.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: An instance of the logged in `ParseUser`.
- throws: An error of type `ParseError`.
*/
func link(authData: [String: String],
options: API.Options = []) async throws -> AuthenticatedUser {
try await withCheckedThrowingContinuation { continuation in
self.link(authData: authData,
options: options,
completion: continuation.resume)
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// ParseSpotify+combine.swift
// ParseSwift
//
// Created by Ulaş Sancak on 06/20/22.
// Copyright © 2022 Parse Community. All rights reserved.
//

#if canImport(Combine)
import Foundation
import Combine

public extension ParseSpotify {
// MARK: Combine
/**
Login a `ParseUser` *asynchronously* using Spotify authentication. Publishes when complete.
- parameter id: The **Spotify profile id** from **Spotify**.
- parameter accessToken: Required **access_token** from **Spotify**.
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: A publisher that eventually produces a single value and then finishes or fails.
*/
func loginPublisher(id: String,
accessToken: String,
expiresIn: Int? = nil,
refreshToken: String? = nil,
options: API.Options = []) -> Future<AuthenticatedUser, ParseError> {
Future { promise in
self.login(id: id,
accessToken: accessToken,
expiresIn: expiresIn,
refreshToken: refreshToken,
options: options,
completion: promise)
}
}

/**
Login a `ParseUser` *asynchronously* using Spotify authentication. Publishes when complete.
- parameter authData: Dictionary containing key/values.
- returns: A publisher that eventually produces a single value and then finishes or fails.
*/
func loginPublisher(authData: [String: String],
options: API.Options = []) -> Future<AuthenticatedUser, ParseError> {
Future { promise in
self.login(authData: authData,
options: options,
completion: promise)
}
}
}

public extension ParseSpotify {
/**
Link the *current* `ParseUser` *asynchronously* using Spotify authentication.
Publishes when complete.
- parameter id: The **Spotify profile id** from **Spotify**.
- parameter accessToken: Required **access_token** from **Spotify**.
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: A publisher that eventually produces a single value and then finishes or fails.
*/
func linkPublisher(id: String,
accessToken: String,
expiresIn: Int? = nil,
refreshToken: String? = nil,
options: API.Options = []) -> Future<AuthenticatedUser, ParseError> {
Future { promise in
self.link(id: id,
accessToken: accessToken,
expiresIn: expiresIn,
refreshToken: refreshToken,
options: options,
completion: promise)
}
}

/**
Link the *current* `ParseUser` *asynchronously* using Spotify authentication.
Publishes when complete.
- parameter authData: Dictionary containing key/values.
- returns: A publisher that eventually produces a single value and then finishes or fails.
*/
func linkPublisher(authData: [String: String],
options: API.Options = []) -> Future<AuthenticatedUser, ParseError> {
Future { promise in
self.link(authData: authData,
options: options,
completion: promise)
}
}
}

#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
//
// ParseSpotify.swift
// ParseSwift
//
// Created by Ulaş Sancak on 06/20/22.
// Copyright © 2022 Parse Community. All rights reserved.
//

import Foundation

// swiftlint:disable line_length

/**
Provides utility functions for working with Spotify User Authentication and `ParseUser`'s.
Be sure your Parse Server is configured for [sign in with Spotify](https://docs.parseplatform.org/parse-server/guide/#spotify-authdata)
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/)
*/
public struct ParseSpotify<AuthenticatedUser: ParseUser>: ParseAuthentication {

/// Authentication keys required for Spotify authentication.
enum AuthenticationKeys: String, Codable {
case id
case accessToken = "access_token"
case expirationDate = "expiration_date"
case refreshToken = "refresh_token"
/// Properly makes an authData dictionary with the required keys.
/// - parameter id: Required id for the user.
/// - parameter accessToken: Required access token for Spotify.
/// - parameter expiresIn: Optional expiration in seconds for Spotify.
/// - parameter refreshToken: Optional refresh token for Spotify.
/// - returns: authData dictionary.
func makeDictionary(id: String,
accessToken: String,
expiresIn: Int? = nil,
refreshToken: String? = nil) -> [String: String] {

var returnDictionary = [
AuthenticationKeys.id.rawValue: id,
AuthenticationKeys.accessToken.rawValue: accessToken
]
if let expiresIn = expiresIn,
let expirationDate = Calendar.current.date(byAdding: .second,
value: expiresIn,
to: Date()) {
let dateString = ParseCoding.dateFormatter.string(from: expirationDate)
returnDictionary[AuthenticationKeys.expirationDate.rawValue] = dateString
}
if let refreshToken = refreshToken {
returnDictionary[AuthenticationKeys.refreshToken.rawValue] = refreshToken
}
return returnDictionary
}

/// Verifies all mandatory keys are in authData.
/// - parameter authData: Dictionary containing key/values.
/// - returns: **true** if all the mandatory keys are present, **false** otherwise.
func verifyMandatoryKeys(authData: [String: String]) -> Bool {
guard authData[AuthenticationKeys.id.rawValue] != nil,
authData[AuthenticationKeys.accessToken.rawValue] != nil else {
return false
}
return true
}
}

public static var __type: String { // swiftlint:disable:this identifier_name
"spotify"
}

public init() { }
}

// MARK: Login
public extension ParseSpotify {

/**
Login a `ParseUser` *asynchronously* using Spotify authentication.
- parameter id: The **Spotify profile id** from **Spotify**.
- parameter accessToken: Required **access_token** from **Spotify**.
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
- parameter completion: The block to execute.
*/
func login(id: String,
accessToken: String,
expiresIn: Int? = nil,
refreshToken: String? = nil,
options: API.Options = [],
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {

let spotifyAuthData = AuthenticationKeys.id
.makeDictionary(id: id,
accessToken: accessToken,
expiresIn: expiresIn,
refreshToken: refreshToken)
login(authData: spotifyAuthData,
options: options,
callbackQueue: callbackQueue,
completion: completion)
}

func login(authData: [String: String],
options: API.Options = [],
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData) else {
callbackQueue.async {
completion(.failure(.init(code: .unknownError,
message: "Should have authData in consisting of keys \"id\" and \"accessToken\".")))
}
return
}
AuthenticatedUser.login(Self.__type,
authData: authData,
options: options,
callbackQueue: callbackQueue,
completion: completion)
}
}

// MARK: Link
public extension ParseSpotify {

/**
Link the *current* `ParseUser` *asynchronously* using Spotify authentication.
- parameter id: The **Spotify profile id** from **Spotify**.
- parameter accessToken: Required **access_token** from **Spotify**.
- parameter expiresIn: Optional **expires_in** in seconds from **Spotify**.
- parameter refreshToken: Optional **refresh_token** from **Spotify**.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
- parameter completion: The block to execute.
*/
func link(id: String,
accessToken: String,
expiresIn: Int? = nil,
refreshToken: String? = nil,
options: API.Options = [],
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
let spotifyAuthData = AuthenticationKeys.id
.makeDictionary(id: id,
accessToken: accessToken,
expiresIn: expiresIn,
refreshToken: refreshToken)
link(authData: spotifyAuthData,
options: options,
callbackQueue: callbackQueue,
completion: completion)
}

func link(authData: [String: String],
options: API.Options = [],
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData) else {
callbackQueue.async {
completion(.failure(.init(code: .unknownError,
message: "Should have authData in consisting of keys \"id\" and \"accessToken\".")))
}
return
}
AuthenticatedUser.link(Self.__type,
authData: authData,
options: options,
callbackQueue: callbackQueue,
completion: completion)
}
}

// MARK: 3rd Party Authentication - ParseSpotify
public extension ParseUser {

/// A Spotify `ParseUser`.
static var spotify: ParseSpotify<Self> {
ParseSpotify<Self>()
}

/// An Spotify `ParseUser`.
var spotify: ParseSpotify<Self> {
Self.spotify
}
}
Loading

0 comments on commit 926fc05

Please sign in to comment.