Skip to content

Commit 7a0540c

Browse files
authored
feat: Add the ability to login users automatically (#98)
* feat: Add the ability to create users automatically * fix automatic login * doc nits * increase codecov * Update ParseVersion.swift
1 parent 2180ca6 commit 7a0540c

File tree

11 files changed

+190
-14
lines changed

11 files changed

+190
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
[Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.4.3...5.5.0), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.5.0/documentation/parseswift)
1010

1111
__New features__
12+
* Adds a setting to enable automatic user login by calling User.current(). The setting can be enabled/disabled when initializing the SDK by setting "usingAutomaticLogin" or at anytime after initialization using User.enableAutomaticLogin() ([#98](https://github.com/netreconlab/Parse-Swift/pull/98)), thanks to [Corey Baker](https://github.com/cbaker6).
1213
* Add ParseServer.information() to retrieve version and info from a Parse Server. Depracates ParseHealth and check() in favor of ParseServer and health() respectively ([#97](https://github.com/netreconlab/Parse-Swift/pull/97)), thanks to [Corey Baker](https://github.com/cbaker6).
1314

1415
### 5.4.3

Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+async.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,15 @@ public extension ParseUser {
8989
}
9090
}
9191

92+
internal static func signupWithAuthData(_ type: String,
93+
authData: [String: String],
94+
options: API.Options = []) async throws -> Self {
95+
try await withCheckedThrowingContinuation { continuation in
96+
Self.signupWithAuthData(type,
97+
authData: authData,
98+
options: options,
99+
completion: continuation.resume)
100+
}
101+
}
102+
92103
}

Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -255,17 +255,31 @@ public extension ParseUser {
255255
callbackQueue: callbackQueue,
256256
completion: completion)
257257
} catch {
258-
let body = SignupLoginBody(authData: [type: authData])
259-
do {
260-
try await signupCommand(body: body)
261-
.execute(options: options,
262-
callbackQueue: callbackQueue,
263-
completion: completion)
264-
} catch {
265-
let parseError = error as? ParseError ?? ParseError(swift: error)
266-
callbackQueue.async {
267-
completion(.failure(parseError))
268-
}
258+
signupWithAuthData(type,
259+
authData: authData,
260+
options: options,
261+
callbackQueue: callbackQueue,
262+
completion: completion)
263+
}
264+
}
265+
}
266+
267+
internal static func signupWithAuthData(_ type: String,
268+
authData: [String: String],
269+
options: API.Options = [],
270+
callbackQueue: DispatchQueue = .main,
271+
completion: @escaping (Result<Self, ParseError>) -> Void) {
272+
let body = SignupLoginBody(authData: [type: authData])
273+
Task {
274+
do {
275+
try await signupCommand(body: body)
276+
.execute(options: options,
277+
callbackQueue: callbackQueue,
278+
completion: completion)
279+
} catch {
280+
let parseError = error as? ParseError ?? ParseError(swift: error)
281+
callbackQueue.async {
282+
completion(.failure(parseError))
269283
}
270284
}
271285
}

Sources/ParseSwift/Objects/ParseUser.swift

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,8 +168,14 @@ public extension ParseUser {
168168
try await yieldIfNotInitialized()
169169
guard let container = await Self.currentContainer(),
170170
let user = container.currentUser else {
171-
throw ParseError(code: .otherCause,
172-
message: "There is no current user logged in")
171+
// User automatic login if configured
172+
guard Parse.configuration.isUsingAutomaticLogin else {
173+
throw ParseError(code: .otherCause,
174+
message: "There is no current user logged in")
175+
}
176+
let authData = ParseAnonymous<Self>.AuthenticationKeys.id.makeDictionary()
177+
return try await Self.loginLazy(ParseAnonymous<Self>().__type,
178+
authData: authData)
173179
}
174180
return user
175181
}
@@ -186,6 +192,12 @@ public extension ParseUser {
186192
currentContainer?.currentUser = newValue
187193
await Self.setCurrentContainer(currentContainer)
188194
}
195+
196+
internal static func loginLazy(_ type: String, authData: [String: String]) async throws -> Self {
197+
try await Self.signupWithAuthData(type,
198+
authData: authData)
199+
}
200+
189201
}
190202

191203
// MARK: SignupLoginBody
@@ -1573,4 +1585,26 @@ public extension Sequence where Element: ParseUser {
15731585
}
15741586
}
15751587
}
1588+
}
1589+
1590+
// MARK: Automatic User
1591+
public extension ParseObject {
1592+
1593+
/**
1594+
Enables/disables automatic creation of anonymous users. After calling this method,
1595+
`Self.current()` will always have a value or throw an error from the server.
1596+
When enabled, the user will only be created on the server once.
1597+
1598+
- parameter enable: **true** allows automatic user logins, **false**
1599+
disables automatic user logins. Defaults to **true**.
1600+
- throws: An error of `ParseError` type.
1601+
*/
1602+
static func enableAutomaticLogin(_ enable: Bool = true) async throws {
1603+
try await yieldIfNotInitialized()
1604+
guard Parse.configuration.isUsingAutomaticLogin != enable else {
1605+
return
1606+
}
1607+
Parse.configuration.isUsingAutomaticLogin = enable
1608+
}
1609+
15761610
} // swiftlint:disable:this file_length

Sources/ParseSwift/Parse.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ internal func initialize(applicationId: String,
2929
usingDataProtectionKeychain: Bool = false,
3030
deletingKeychainIfNeeded: Bool = false,
3131
httpAdditionalHeaders: [AnyHashable: Any]? = nil,
32+
usingAutomaticLogin: Bool = false,
3233
maxConnectionAttempts: Int = 5,
3334
liveQueryMaxConnectionAttempts: Int = 20,
3435
testing: Bool = false,
@@ -52,6 +53,7 @@ internal func initialize(applicationId: String,
5253
usingDataProtectionKeychain: usingDataProtectionKeychain,
5354
deletingKeychainIfNeeded: deletingKeychainIfNeeded,
5455
httpAdditionalHeaders: httpAdditionalHeaders,
56+
usingAutomaticLogin: usingAutomaticLogin,
5557
maxConnectionAttempts: maxConnectionAttempts,
5658
liveQueryMaxConnectionAttempts: liveQueryMaxConnectionAttempts,
5759
authentication: authentication)
@@ -235,6 +237,9 @@ public func initialize(configuration: ParseConfiguration) async throws { // swif
235237
- parameter httpAdditionalHeaders: A dictionary of additional headers to send with requests. See Apple's
236238
[documentation](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411532-httpadditionalheaders)
237239
for more info.
240+
- parameter usingAutomaticLogin: If **true**, automatic creation of anonymous users is enabled.
241+
When enabled, `User.current()` will always have a value or throw an error from the server. The user will only be created on
242+
the server once.
238243
- parameter maxConnectionAttempts: Maximum number of times to try to connect to Parse Server.
239244
Defaults to 5.
240245
- parameter liveQueryMaxConnectionAttempts: Maximum number of times to try to connect to a Parse
@@ -270,6 +275,7 @@ public func initialize(
270275
usingDataProtectionKeychain: Bool = false,
271276
deletingKeychainIfNeeded: Bool = false,
272277
httpAdditionalHeaders: [AnyHashable: Any]? = nil,
278+
usingAutomaticLogin: Bool = false,
273279
maxConnectionAttempts: Int = 5,
274280
liveQueryMaxConnectionAttempts: Int = 20,
275281
parseFileTransfer: ParseFileTransferable? = nil,
@@ -293,6 +299,7 @@ public func initialize(
293299
usingDataProtectionKeychain: usingDataProtectionKeychain,
294300
deletingKeychainIfNeeded: deletingKeychainIfNeeded,
295301
httpAdditionalHeaders: httpAdditionalHeaders,
302+
usingAutomaticLogin: usingAutomaticLogin,
296303
maxConnectionAttempts: maxConnectionAttempts,
297304
liveQueryMaxConnectionAttempts: liveQueryMaxConnectionAttempts,
298305
parseFileTransfer: parseFileTransfer,

Sources/ParseSwift/Types/ParseConfiguration.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ public struct ParseConfiguration {
9191
/// apps do not have credentials to setup a Keychain.
9292
public internal(set) var isUsingDataProtectionKeychain: Bool = false
9393

94+
/// If **true**, automatic creation of anonymous users is enabled.
95+
/// When enabled, `User.current()` will always have a value or throw an error from the server.
96+
/// The user will only be created on the server once.
97+
/// Defaults to **false**.
98+
public internal(set) var isUsingAutomaticLogin: Bool = false
99+
94100
/// Maximum number of times to try to connect to a Parse Server.
95101
/// Defaults to 5.
96102
public internal(set) var maxConnectionAttempts: Int = 5
@@ -147,6 +153,9 @@ public struct ParseConfiguration {
147153
- parameter httpAdditionalHeaders: A dictionary of additional headers to send with requests. See Apple's
148154
[documentation](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411532-httpadditionalheaders)
149155
for more info.
156+
- parameter usingAutomaticLogin: If **true**, automatic creation of anonymous users is enabled.
157+
When enabled, `User.current()` will always have a value or throw an error from the server. The user will only be created on
158+
the server once.
150159
- parameter maxConnectionAttempts: Maximum number of times to try to connect to a Parse Server.
151160
Defaults to 5.
152161
- parameter liveQueryMaxConnectionAttempts: Maximum number of times to try to connect to a Parse
@@ -181,6 +190,7 @@ public struct ParseConfiguration {
181190
usingDataProtectionKeychain: Bool = false,
182191
deletingKeychainIfNeeded: Bool = false,
183192
httpAdditionalHeaders: [AnyHashable: Any]? = nil,
193+
usingAutomaticLogin: Bool = false,
184194
maxConnectionAttempts: Int = 5,
185195
liveQueryMaxConnectionAttempts: Int = 20,
186196
parseFileTransfer: ParseFileTransferable? = nil,
@@ -206,6 +216,7 @@ public struct ParseConfiguration {
206216
self.isUsingDataProtectionKeychain = usingDataProtectionKeychain
207217
self.isDeletingKeychainIfNeeded = deletingKeychainIfNeeded
208218
self.httpAdditionalHeaders = httpAdditionalHeaders
219+
self.isUsingAutomaticLogin = usingAutomaticLogin
209220
self.maxConnectionAttempts = maxConnectionAttempts
210221
self.liveQueryMaxConnectionAttempts = liveQueryMaxConnectionAttempts
211222
self.parseFileTransfer = parseFileTransfer ?? ParseFileDefaultTransfer()

Sources/ParseSwift/Types/ParseVersion.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public struct ParseVersion: ParseTypeable, Hashable {
172172
}
173173

174174
// MARK: Default Implementation
175-
extension ParseVersion {
175+
public extension ParseVersion {
176176

177177
init(string: String) throws {
178178
self = try Self.convertVersionString(string)

Tests/ParseSwiftTests/ParseAnonymousTests.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ class ParseAnonymousTests: XCTestCase {
124124
XCTAssertNotEqual(authData["id"], "12345")
125125
}
126126

127+
@MainActor
127128
func testLogin() async throws {
128129
var serverResponse = LoginSignupResponse()
129130
let authData = ParseAnonymous<User>.AuthenticationKeys.id.makeDictionary()
@@ -160,6 +161,60 @@ class ParseAnonymousTests: XCTestCase {
160161
XCTAssertTrue(isLinked)
161162
}
162163

164+
@MainActor
165+
func testLoginAutomaticLogin() async throws {
166+
try await User.enableAutomaticLogin()
167+
XCTAssertTrue(Parse.configuration.isUsingAutomaticLogin)
168+
169+
var serverResponse = LoginSignupResponse()
170+
let authData = ParseAnonymous<User>.AuthenticationKeys.id.makeDictionary()
171+
serverResponse.username = "hello"
172+
serverResponse.password = "world"
173+
serverResponse.objectId = "yarr"
174+
serverResponse.sessionToken = "myToken"
175+
serverResponse.authData = [serverResponse.anonymous.__type: authData]
176+
serverResponse.createdAt = Date()
177+
serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300)
178+
179+
var userOnServer: User
180+
181+
var encoded: Data
182+
do {
183+
encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none)
184+
// Get dates in correct format from ParseDecoding strategy
185+
userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded)
186+
} catch {
187+
XCTFail("Should encode/decode. Error \(error)")
188+
return
189+
}
190+
MockURLProtocol.mockRequests { _ in
191+
return MockURLResponse(data: encoded, statusCode: 200)
192+
}
193+
194+
let currentUser = try await User.current()
195+
XCTAssertEqual(currentUser, userOnServer)
196+
XCTAssertEqual(currentUser.username, "hello")
197+
XCTAssertEqual(currentUser.password, "world")
198+
let isLinked = await currentUser.anonymous.isLinked()
199+
XCTAssertTrue(isLinked)
200+
201+
// User stays the same and does not access server when logged in already
202+
serverResponse.objectId = "peace"
203+
do {
204+
encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none)
205+
// Get dates in correct format from ParseDecoding strategy
206+
userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded)
207+
} catch {
208+
XCTFail("Should encode/decode. Error \(error)")
209+
return
210+
}
211+
MockURLProtocol.mockRequests { _ in
212+
return MockURLResponse(data: encoded, statusCode: 200)
213+
}
214+
let currentUser2 = try await User.current()
215+
XCTAssertEqual(currentUser, currentUser2)
216+
}
217+
163218
@MainActor
164219
func testLoginAuthData() async throws {
165220
var serverResponse = LoginSignupResponse()

Tests/ParseSwiftTests/ParseAppleTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,4 +520,35 @@ class ParseAppleTests: XCTestCase {
520520
XCTAssertNil(updatedUser.password)
521521
XCTAssertFalse(ParseApple<User>.isLinked(with: updatedUser))
522522
}
523+
524+
@MainActor
525+
func testUnlinkNotLoggedIn() async throws {
526+
do {
527+
_ = try await User.apple.unlink()
528+
XCTFail("Should have thrown error")
529+
} catch {
530+
XCTAssertTrue(error.containedIn([.invalidLinkedSession]))
531+
let isLinked = await User.apple.isLinked()
532+
XCTAssertFalse(isLinked)
533+
}
534+
}
535+
536+
func testUnlinkNotLoggedInCompletion() async throws {
537+
let expectation1 = XCTestExpectation(description: "Wait 1")
538+
User.apple.unlink { result in
539+
switch result {
540+
case .success:
541+
XCTFail("Should have produced error")
542+
case .failure(let error):
543+
XCTAssertEqual(error.code, .invalidLinkedSession)
544+
}
545+
expectation1.fulfill()
546+
}
547+
#if compiler(>=5.8.0) && !os(Linux) && !os(Android) && !os(Windows)
548+
await fulfillment(of: [expectation1], timeout: 20.0)
549+
#elseif compiler(<5.8.0) && !os(iOS) && !os(tvOS)
550+
wait(for: [expectation1], timeout: 20.0)
551+
#endif
552+
}
553+
523554
}

Tests/ParseSwiftTests/ParseAuthenticationAsyncTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,5 @@ class ParseAuthenticationAsyncTests: XCTestCase {
223223
XCTAssertEqual(user.username, "hello10")
224224
XCTAssertNil(user.password)
225225
}
226+
226227
}

0 commit comments

Comments
 (0)