Skip to content

Commit

Permalink
feat: Add the ability to login users automatically (#98)
Browse files Browse the repository at this point in the history
* feat: Add the ability to create users automatically

* fix automatic login

* doc nits

* increase codecov

* Update ParseVersion.swift
  • Loading branch information
cbaker6 committed May 9, 2023
1 parent 2180ca6 commit 7a0540c
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
[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)

__New features__
* 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).
* 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).

### 5.4.3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,15 @@ public extension ParseUser {
}
}

internal static func signupWithAuthData(_ type: String,
authData: [String: String],
options: API.Options = []) async throws -> Self {
try await withCheckedThrowingContinuation { continuation in
Self.signupWithAuthData(type,
authData: authData,
options: options,
completion: continuation.resume)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -255,17 +255,31 @@ public extension ParseUser {
callbackQueue: callbackQueue,
completion: completion)
} catch {
let body = SignupLoginBody(authData: [type: authData])
do {
try await signupCommand(body: body)
.execute(options: options,
callbackQueue: callbackQueue,
completion: completion)
} catch {
let parseError = error as? ParseError ?? ParseError(swift: error)
callbackQueue.async {
completion(.failure(parseError))
}
signupWithAuthData(type,
authData: authData,
options: options,
callbackQueue: callbackQueue,
completion: completion)
}
}
}

internal static func signupWithAuthData(_ type: String,
authData: [String: String],
options: API.Options = [],
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<Self, ParseError>) -> Void) {
let body = SignupLoginBody(authData: [type: authData])
Task {
do {
try await signupCommand(body: body)
.execute(options: options,
callbackQueue: callbackQueue,
completion: completion)
} catch {
let parseError = error as? ParseError ?? ParseError(swift: error)
callbackQueue.async {
completion(.failure(parseError))
}
}
}
Expand Down
38 changes: 36 additions & 2 deletions Sources/ParseSwift/Objects/ParseUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,14 @@ public extension ParseUser {
try await yieldIfNotInitialized()
guard let container = await Self.currentContainer(),
let user = container.currentUser else {
throw ParseError(code: .otherCause,
message: "There is no current user logged in")
// User automatic login if configured
guard Parse.configuration.isUsingAutomaticLogin else {
throw ParseError(code: .otherCause,
message: "There is no current user logged in")
}
let authData = ParseAnonymous<Self>.AuthenticationKeys.id.makeDictionary()
return try await Self.loginLazy(ParseAnonymous<Self>().__type,
authData: authData)
}
return user
}
Expand All @@ -186,6 +192,12 @@ public extension ParseUser {
currentContainer?.currentUser = newValue
await Self.setCurrentContainer(currentContainer)
}

internal static func loginLazy(_ type: String, authData: [String: String]) async throws -> Self {
try await Self.signupWithAuthData(type,
authData: authData)
}

}

// MARK: SignupLoginBody
Expand Down Expand Up @@ -1573,4 +1585,26 @@ public extension Sequence where Element: ParseUser {
}
}
}
}

// MARK: Automatic User
public extension ParseObject {

/**
Enables/disables automatic creation of anonymous users. After calling this method,
`Self.current()` will always have a value or throw an error from the server.
When enabled, the user will only be created on the server once.
- parameter enable: **true** allows automatic user logins, **false**
disables automatic user logins. Defaults to **true**.
- throws: An error of `ParseError` type.
*/
static func enableAutomaticLogin(_ enable: Bool = true) async throws {
try await yieldIfNotInitialized()
guard Parse.configuration.isUsingAutomaticLogin != enable else {
return
}
Parse.configuration.isUsingAutomaticLogin = enable
}

} // swiftlint:disable:this file_length
7 changes: 7 additions & 0 deletions Sources/ParseSwift/Parse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ internal func initialize(applicationId: String,
usingDataProtectionKeychain: Bool = false,
deletingKeychainIfNeeded: Bool = false,
httpAdditionalHeaders: [AnyHashable: Any]? = nil,
usingAutomaticLogin: Bool = false,
maxConnectionAttempts: Int = 5,
liveQueryMaxConnectionAttempts: Int = 20,
testing: Bool = false,
Expand All @@ -52,6 +53,7 @@ internal func initialize(applicationId: String,
usingDataProtectionKeychain: usingDataProtectionKeychain,
deletingKeychainIfNeeded: deletingKeychainIfNeeded,
httpAdditionalHeaders: httpAdditionalHeaders,
usingAutomaticLogin: usingAutomaticLogin,
maxConnectionAttempts: maxConnectionAttempts,
liveQueryMaxConnectionAttempts: liveQueryMaxConnectionAttempts,
authentication: authentication)
Expand Down Expand Up @@ -235,6 +237,9 @@ public func initialize(configuration: ParseConfiguration) async throws { // swif
- parameter httpAdditionalHeaders: A dictionary of additional headers to send with requests. See Apple's
[documentation](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411532-httpadditionalheaders)
for more info.
- parameter usingAutomaticLogin: If **true**, automatic creation of anonymous users is enabled.
When enabled, `User.current()` will always have a value or throw an error from the server. The user will only be created on
the server once.
- parameter maxConnectionAttempts: Maximum number of times to try to connect to Parse Server.
Defaults to 5.
- parameter liveQueryMaxConnectionAttempts: Maximum number of times to try to connect to a Parse
Expand Down Expand Up @@ -270,6 +275,7 @@ public func initialize(
usingDataProtectionKeychain: Bool = false,
deletingKeychainIfNeeded: Bool = false,
httpAdditionalHeaders: [AnyHashable: Any]? = nil,
usingAutomaticLogin: Bool = false,
maxConnectionAttempts: Int = 5,
liveQueryMaxConnectionAttempts: Int = 20,
parseFileTransfer: ParseFileTransferable? = nil,
Expand All @@ -293,6 +299,7 @@ public func initialize(
usingDataProtectionKeychain: usingDataProtectionKeychain,
deletingKeychainIfNeeded: deletingKeychainIfNeeded,
httpAdditionalHeaders: httpAdditionalHeaders,
usingAutomaticLogin: usingAutomaticLogin,
maxConnectionAttempts: maxConnectionAttempts,
liveQueryMaxConnectionAttempts: liveQueryMaxConnectionAttempts,
parseFileTransfer: parseFileTransfer,
Expand Down
11 changes: 11 additions & 0 deletions Sources/ParseSwift/Types/ParseConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ public struct ParseConfiguration {
/// apps do not have credentials to setup a Keychain.
public internal(set) var isUsingDataProtectionKeychain: Bool = false

/// If **true**, automatic creation of anonymous users is enabled.
/// When enabled, `User.current()` will always have a value or throw an error from the server.
/// The user will only be created on the server once.
/// Defaults to **false**.
public internal(set) var isUsingAutomaticLogin: Bool = false

/// Maximum number of times to try to connect to a Parse Server.
/// Defaults to 5.
public internal(set) var maxConnectionAttempts: Int = 5
Expand Down Expand Up @@ -147,6 +153,9 @@ public struct ParseConfiguration {
- parameter httpAdditionalHeaders: A dictionary of additional headers to send with requests. See Apple's
[documentation](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411532-httpadditionalheaders)
for more info.
- parameter usingAutomaticLogin: If **true**, automatic creation of anonymous users is enabled.
When enabled, `User.current()` will always have a value or throw an error from the server. The user will only be created on
the server once.
- parameter maxConnectionAttempts: Maximum number of times to try to connect to a Parse Server.
Defaults to 5.
- parameter liveQueryMaxConnectionAttempts: Maximum number of times to try to connect to a Parse
Expand Down Expand Up @@ -181,6 +190,7 @@ public struct ParseConfiguration {
usingDataProtectionKeychain: Bool = false,
deletingKeychainIfNeeded: Bool = false,
httpAdditionalHeaders: [AnyHashable: Any]? = nil,
usingAutomaticLogin: Bool = false,
maxConnectionAttempts: Int = 5,
liveQueryMaxConnectionAttempts: Int = 20,
parseFileTransfer: ParseFileTransferable? = nil,
Expand All @@ -206,6 +216,7 @@ public struct ParseConfiguration {
self.isUsingDataProtectionKeychain = usingDataProtectionKeychain
self.isDeletingKeychainIfNeeded = deletingKeychainIfNeeded
self.httpAdditionalHeaders = httpAdditionalHeaders
self.isUsingAutomaticLogin = usingAutomaticLogin
self.maxConnectionAttempts = maxConnectionAttempts
self.liveQueryMaxConnectionAttempts = liveQueryMaxConnectionAttempts
self.parseFileTransfer = parseFileTransfer ?? ParseFileDefaultTransfer()
Expand Down
2 changes: 1 addition & 1 deletion Sources/ParseSwift/Types/ParseVersion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public struct ParseVersion: ParseTypeable, Hashable {
}

// MARK: Default Implementation
extension ParseVersion {
public extension ParseVersion {

init(string: String) throws {
self = try Self.convertVersionString(string)
Expand Down
55 changes: 55 additions & 0 deletions Tests/ParseSwiftTests/ParseAnonymousTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class ParseAnonymousTests: XCTestCase {
XCTAssertNotEqual(authData["id"], "12345")
}

@MainActor
func testLogin() async throws {
var serverResponse = LoginSignupResponse()
let authData = ParseAnonymous<User>.AuthenticationKeys.id.makeDictionary()
Expand Down Expand Up @@ -160,6 +161,60 @@ class ParseAnonymousTests: XCTestCase {
XCTAssertTrue(isLinked)
}

@MainActor
func testLoginAutomaticLogin() async throws {
try await User.enableAutomaticLogin()
XCTAssertTrue(Parse.configuration.isUsingAutomaticLogin)

var serverResponse = LoginSignupResponse()
let authData = ParseAnonymous<User>.AuthenticationKeys.id.makeDictionary()
serverResponse.username = "hello"
serverResponse.password = "world"
serverResponse.objectId = "yarr"
serverResponse.sessionToken = "myToken"
serverResponse.authData = [serverResponse.anonymous.__type: authData]
serverResponse.createdAt = Date()
serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300)

var userOnServer: User

var encoded: Data
do {
encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none)
// Get dates in correct format from ParseDecoding strategy
userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded)
} catch {
XCTFail("Should encode/decode. Error \(error)")
return
}
MockURLProtocol.mockRequests { _ in
return MockURLResponse(data: encoded, statusCode: 200)
}

let currentUser = try await User.current()
XCTAssertEqual(currentUser, userOnServer)
XCTAssertEqual(currentUser.username, "hello")
XCTAssertEqual(currentUser.password, "world")
let isLinked = await currentUser.anonymous.isLinked()
XCTAssertTrue(isLinked)

// User stays the same and does not access server when logged in already
serverResponse.objectId = "peace"
do {
encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none)
// Get dates in correct format from ParseDecoding strategy
userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded)
} catch {
XCTFail("Should encode/decode. Error \(error)")
return
}
MockURLProtocol.mockRequests { _ in
return MockURLResponse(data: encoded, statusCode: 200)
}
let currentUser2 = try await User.current()
XCTAssertEqual(currentUser, currentUser2)
}

@MainActor
func testLoginAuthData() async throws {
var serverResponse = LoginSignupResponse()
Expand Down
31 changes: 31 additions & 0 deletions Tests/ParseSwiftTests/ParseAppleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -520,4 +520,35 @@ class ParseAppleTests: XCTestCase {
XCTAssertNil(updatedUser.password)
XCTAssertFalse(ParseApple<User>.isLinked(with: updatedUser))
}

@MainActor
func testUnlinkNotLoggedIn() async throws {
do {
_ = try await User.apple.unlink()
XCTFail("Should have thrown error")
} catch {
XCTAssertTrue(error.containedIn([.invalidLinkedSession]))
let isLinked = await User.apple.isLinked()
XCTAssertFalse(isLinked)
}
}

func testUnlinkNotLoggedInCompletion() async throws {
let expectation1 = XCTestExpectation(description: "Wait 1")
User.apple.unlink { result in
switch result {
case .success:
XCTFail("Should have produced error")
case .failure(let error):
XCTAssertEqual(error.code, .invalidLinkedSession)
}
expectation1.fulfill()
}
#if compiler(>=5.8.0) && !os(Linux) && !os(Android) && !os(Windows)
await fulfillment(of: [expectation1], timeout: 20.0)
#elseif compiler(<5.8.0) && !os(iOS) && !os(tvOS)
wait(for: [expectation1], timeout: 20.0)
#endif
}

}
1 change: 1 addition & 0 deletions Tests/ParseSwiftTests/ParseAuthenticationAsyncTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,4 +223,5 @@ class ParseAuthenticationAsyncTests: XCTestCase {
XCTAssertEqual(user.username, "hello10")
XCTAssertNil(user.password)
}

}
11 changes: 11 additions & 0 deletions Tests/ParseSwiftTests/ParseUserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2543,5 +2543,16 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length
}
}
}

func testEnableAutomaticLogin() async throws {
XCTAssertFalse(Parse.configuration.isUsingAutomaticLogin)
try await User.enableAutomaticLogin(false)
XCTAssertFalse(Parse.configuration.isUsingAutomaticLogin)
try await User.enableAutomaticLogin()
XCTAssertTrue(Parse.configuration.isUsingAutomaticLogin)
try await User.enableAutomaticLogin(false)
XCTAssertFalse(Parse.configuration.isUsingAutomaticLogin)
}

}
// swiftlint:disable:this file_length

0 comments on commit 7a0540c

Please sign in to comment.