Skip to content

Commit 5d15b43

Browse files
cbaker6TomWFox
andauthored
Anonymous and Apple Login (#53)
* Initial * Update * Add ParseUser.become * Added some playground examples. Bug fixes * Update docs * Fix Swift 5.2 closure issue * Update ParseApple docs * Update docs * update signupCommand with authData if none sent back from server. * Moved some methods to instance * Clean up convenience methods * Improve folder structure * Fixed unlinking/stripping of auths. Also removed reqs for implementing sync calls for auths as they shouldn't be used anyways. Devs can use semaphores/etc if they want sync * Added support for potential Linux build * Add some tests * Improvements * Add testcases * Add Session protocol * Remove session endpoints as they are not used for now. * Clean up * Fix unlink async callback * Apply suggestions from code review Co-authored-by: Tom Fox <[email protected]> * Make Session "restricted" key optional * try updated lock file to fix docs in CI * See if jazzy stops giving issues with older tools * try older version while running jazzy * See if Xcode 12.2 allows building of docs Co-authored-by: Tom Fox <[email protected]>
1 parent 84f9231 commit 5d15b43

34 files changed

+2159
-79
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
branches: '*'
77
env:
88
CI_XCODE_VER: '/Applications/Xcode_11.7.app/Contents/Developer'
9+
CI_XCODE_VER_12: '/Applications/Xcode_12.2.app/Contents/Developer'
910

1011
jobs:
1112
xcode-test-ios:
@@ -104,8 +105,12 @@ jobs:
104105
run: |
105106
bundle config path vendor/bundle
106107
bundle install
108+
env:
109+
DEVELOPER_DIR: ${{ env.CI_XCODE_VER_12 }}
107110
- name: Create Jazzy Docs
108111
run: ./Scripts/jazzy.sh
112+
env:
113+
DEVELOPER_DIR: ${{ env.CI_XCODE_VER_12 }}
109114
- name: Deploy Jazzy Docs
110115
if: github.ref == 'refs/heads/main'
111116
uses: peaceiris/actions-gh-pages@v3

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ GEM
5656
escape (0.0.4)
5757
ethon (0.12.0)
5858
ffi (>= 1.3.0)
59-
ffi (1.13.1)
59+
ffi (1.14.2)
6060
fourflusher (2.3.1)
6161
fuzzy_match (2.0.4)
6262
gh_inspector (1.1.3)

ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ struct User: ParseUser {
1818
var username: String?
1919
var email: String?
2020
var password: String?
21+
var authData: [String: [String: String]?]?
2122

2223
//: Your custom keys
2324
var customKey: String?
@@ -76,6 +77,30 @@ do {
7677
print("Error logging out: \(error)")
7778
}
7879

80+
//: Logging in anonymously
81+
User.anonymous.login { result in
82+
switch result {
83+
case .success:
84+
print("Successfully logged in \(User.current)")
85+
case .failure(let error):
86+
print("Error logging in: \(error)")
87+
}
88+
}
89+
90+
//: Convert the anonymous user to a real new user.
91+
User.current?.username = "bye"
92+
User.current?.password = "world"
93+
User.current?.signup { result in
94+
switch result {
95+
96+
case .success(let user):
97+
print("Parse signup successful: \(user)")
98+
99+
case .failure(let error):
100+
print("Error logging in: \(error)")
101+
}
102+
}
103+
79104
//: Password Reset Request - synchronously
80105
do {
81106
try User.verificationEmailRequest(email: "[email protected]")
@@ -92,22 +117,6 @@ do {
92117
print("Error requesting password reset: \(error)")
93118
}
94119

95-
//: Another way to sign up
96-
var newUser = User()
97-
newUser.username = "hello10"
98-
newUser.password = "world"
99-
100-
newUser.signup { result in
101-
switch result {
102-
103-
case .success(let user):
104-
print("Parse signup successful: \(user)")
105-
106-
case .failure(let error):
107-
print("Error logging in: \(error)")
108-
}
109-
}
110-
111120
PlaygroundPage.current.finishExecution()
112121

113122
//: [Next](@next)

ParseSwift.xcodeproj/project.pbxproj

Lines changed: 109 additions & 5 deletions
Large diffs are not rendered by default.

Sources/ParseSwift/API/Responses.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ internal struct LoginSignupResponse: Codable {
119119
let objectId: String
120120
let sessionToken: String
121121
var updatedAt: Date?
122+
let username: String?
122123
}
123124

124125
// MARK: ParseFile
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
//
2+
// ParseApple.swift
3+
// ParseSwift
4+
//
5+
// Created by Corey Baker on 1/14/21.
6+
// Copyright © 2021 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 Apple User Authentication and `ParseUser`'s.
15+
Be sure your Parse Server is configured for [sign in with Apple](https://docs.parseplatform.org/parse-server/guide/#configuring-parse-server-for-sign-in-with-apple).
16+
For information on acquiring Apple sign-in credentials to use with `ParseApple`, refer to [Apple's Documentation](https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple).
17+
*/
18+
public struct ParseApple<AuthenticatedUser: ParseUser>: ParseAuthenticatable {
19+
20+
/// Authentication keys required for Apple authentication.
21+
enum AuthenticationKeys: String, Codable {
22+
case id // swiftlint:disable:this identifier_name
23+
case token
24+
25+
/// Properly makes an authData dictionary with the required keys.
26+
/// - parameter id: Required id.
27+
/// - parameter token: Required token.
28+
/// - returns: Required authData dictionary.
29+
func makeDictionary(user: String,
30+
identityToken: String) -> [String: String] {
31+
[AuthenticationKeys.id.rawValue: user,
32+
AuthenticationKeys.token.rawValue: identityToken]
33+
}
34+
35+
/// Verifies all mandatory keys are in authData.
36+
/// - parameter authData: Dictionary containing key/values.
37+
/// - returns: `true` if all the mandatory keys are present, `false` otherwise.
38+
func verifyMandatoryKeys(authData: [String: String]?) -> Bool {
39+
guard let authData = authData,
40+
authData[AuthenticationKeys.id.rawValue] != nil,
41+
authData[AuthenticationKeys.token.rawValue] != nil else {
42+
return false
43+
}
44+
return true
45+
}
46+
}
47+
public static var __type: String { // swiftlint:disable:this identifier_name
48+
"apple"
49+
}
50+
public init() { }
51+
}
52+
53+
// MARK: Login
54+
public extension ParseApple {
55+
/**
56+
Login a `ParseUser` *asynchronously* using Apple authentication.
57+
- parameter user: The `user` from `ASAuthorizationAppleIDCredential`.
58+
- parameter identityToken: The `identityToken` from `ASAuthorizationAppleIDCredential`.
59+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
60+
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
61+
- parameter completion: The block to execute.
62+
*/
63+
func login(user: String,
64+
identityToken: String,
65+
options: API.Options = [],
66+
callbackQueue: DispatchQueue = .main,
67+
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
68+
login(authData: AuthenticationKeys.id.makeDictionary(user: user, identityToken: identityToken),
69+
options: options,
70+
callbackQueue: callbackQueue,
71+
completion: completion)
72+
}
73+
74+
func login(authData: [String: String]?,
75+
options: API.Options = [],
76+
callbackQueue: DispatchQueue = .main,
77+
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
78+
guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData),
79+
let authData = authData else {
80+
let error = ParseError(code: .unknownError,
81+
message: "Should have authData in consisting of keys \"id\" and \"token\".")
82+
callbackQueue.async {
83+
completion(.failure(error))
84+
}
85+
return
86+
}
87+
AuthenticatedUser.login(Self.__type,
88+
authData: authData,
89+
options: options,
90+
callbackQueue: callbackQueue,
91+
completion: completion)
92+
}
93+
}
94+
95+
// MARK: Link
96+
public extension ParseApple {
97+
98+
/**
99+
Link the *current* `ParseUser` *asynchronously* using Apple authentication.
100+
- parameter user: The `user` from `ASAuthorizationAppleIDCredential`.
101+
- parameter identityToken: The `identityToken` from `ASAuthorizationAppleIDCredential`.
102+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
103+
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
104+
- parameter completion: The block to execute.
105+
*/
106+
func link(user: String,
107+
identityToken: String,
108+
options: API.Options = [],
109+
callbackQueue: DispatchQueue = .main,
110+
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
111+
link(authData: AuthenticationKeys.id.makeDictionary(user: user, identityToken: identityToken),
112+
options: options,
113+
callbackQueue: callbackQueue,
114+
completion: completion)
115+
}
116+
117+
func link(authData: [String: String]?,
118+
options: API.Options = [],
119+
callbackQueue: DispatchQueue = .main,
120+
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
121+
guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData),
122+
let authData = authData else {
123+
let error = ParseError(code: .unknownError,
124+
message: "Should have authData in consisting of keys \"id\" and \"token\".")
125+
callbackQueue.async {
126+
completion(.failure(error))
127+
}
128+
return
129+
}
130+
AuthenticatedUser.link(Self.__type,
131+
authData: authData,
132+
options: options,
133+
callbackQueue: callbackQueue,
134+
completion: completion)
135+
}
136+
}
137+
138+
// MARK: 3rd Party Authentication - ParseApple
139+
public extension ParseUser {
140+
141+
/// An apple `ParseUser`.
142+
static var apple: ParseApple<Self> {
143+
ParseApple<Self>()
144+
}
145+
146+
/// An apple `ParseUser`.
147+
var apple: ParseApple<Self> {
148+
Self.apple
149+
}
150+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//
2+
// ParseAnonymous.swift
3+
// ParseSwift
4+
//
5+
// Created by Corey Baker on 1/14/21.
6+
// Copyright © 2021 Parse Community. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/**
12+
Provides utility functions for working with Anonymously logged-in users.
13+
14+
Anonymous users have some unique characteristics:
15+
- Anonymous users don't need a user name or password.
16+
- Once logged out, an anonymous user cannot be recovered.
17+
- When the current user is anonymous, the following methods can be used to switch
18+
to a different user or convert the anonymous user into a regular one:
19+
- *signup* converts an anonymous user to a standard user with the given username and password.
20+
Data associated with the anonymous user is retained.
21+
- *login* switches users without converting the anonymous user.
22+
Data associated with the anonymous user will be lost.
23+
- Service *login* (e.g. Apple, Facebook, Twitter) will attempt to convert
24+
the anonymous user into a standard user by linking it to the service.
25+
If a user already exists that is linked to the service, it will instead switch to the existing user.
26+
- Service linking (e.g. Apple, Facebook, Twitter) will convert the anonymous user
27+
into a standard user by linking it to the service.
28+
*/
29+
public struct ParseAnonymous<AuthenticatedUser: ParseUser>: ParseAuthenticatable {
30+
31+
enum AuthenticationKeys: String, Codable {
32+
case id // swiftlint:disable:this identifier_name
33+
34+
func makeDictionary() -> [String: String] {
35+
[AuthenticationKeys.id.rawValue: UUID().uuidString.lowercased()]
36+
}
37+
}
38+
public static var __type: String { // swiftlint:disable:this identifier_name
39+
"anonymous"
40+
}
41+
public init() { }
42+
}
43+
44+
// MARK: Login
45+
public extension ParseAnonymous {
46+
/**
47+
Login a `ParseUser` *synchronously* using the respective authentication type.
48+
- parameter authData: The authData for the respective authentication type.
49+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
50+
- throws: `ParseError`.
51+
- returns: the linked `ParseUser`.
52+
*/
53+
func login(authData: [String: String]? = nil,
54+
options: API.Options = []) throws -> AuthenticatedUser {
55+
return try AuthenticatedUser
56+
.login(__type,
57+
authData: AuthenticationKeys.id.makeDictionary(),
58+
options: options)
59+
}
60+
61+
func login(authData: [String: String]? = nil,
62+
options: API.Options = [],
63+
callbackQueue: DispatchQueue = .main,
64+
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
65+
AuthenticatedUser.login(__type,
66+
authData: AuthenticationKeys.id.makeDictionary(),
67+
options: options,
68+
callbackQueue: callbackQueue,
69+
completion: completion)
70+
}
71+
}
72+
73+
// MARK: Link
74+
public extension ParseAnonymous {
75+
/// Unavailable for `ParseAnonymous`. Will always return an error.
76+
func link(authData: [String: String]? = nil,
77+
options: API.Options = [],
78+
callbackQueue: DispatchQueue = .main,
79+
completion: @escaping (Result<AuthenticatedUser, ParseError>) -> Void) {
80+
callbackQueue.async {
81+
completion(.failure(ParseError(code: .unknownError, message: "Not supported")))
82+
}
83+
}
84+
}
85+
86+
// MARK: ParseAnonymous
87+
public extension ParseUser {
88+
89+
/// An anonymous `ParseUser`.
90+
static var anonymous: ParseAnonymous<Self> {
91+
ParseAnonymous<Self>()
92+
}
93+
94+
/// An anonymous `ParseUser`.
95+
var anonymous: ParseAnonymous<Self> {
96+
Self.anonymous
97+
}
98+
}

0 commit comments

Comments
 (0)