Skip to content

feat: add become installation method & fix loginUsingObjCKeychain #407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Sep 12, 2022
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.10.0...main)
* _Contributing to this repo? Add info about your change here to be included in the next release_

__New features__
- Add the become method to ParseInstallation which allows any ParseInstallation to be copied to the current installation. This method can be used to migrate any ParseInstallation to the current installation in the Swift SDK ([#407](https://github.com/parse-community/Parse-Swift/pull/407)), thanks to [Corey Baker](https://github.com/cbaker6).

__Fixes__
- Properly get the session token from the Parse Objective-C Keychain when using ParseUser.loginUsingObjCKeychain ([#407](https://github.com/parse-community/Parse-Swift/pull/407)), thanks to [Corey Baker](https://github.com/cbaker6).

### 4.11.0
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.10.0...4.11.0)

Expand Down
37 changes: 35 additions & 2 deletions Sources/ParseSwift/Objects/ParseInstallation+async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,36 @@ public extension ParseInstallation {
throw error
}
}

/**
Copy the `ParseInstallation` *asynchronously* based on the `objectId`.
On success, this saves the `ParseInstallation` to the keychain, so you can retrieve
the current installation using *current*.

- parameter objectId: The **id** of the `ParseInstallation` to become.
- parameter copyEntireInstallation: When **true**, copies the entire `ParseInstallation`.
When **false**, only the `channels` and `deviceToken` are copied; resulting in a new
`ParseInstallation` for original `sessionToken`. Defaults to **true**.
- 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.
It should have the following argument signature: `(Result<Self, ParseError>)`.
- note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
desires a different policy, it should be inserted in `options`.
*/
@discardableResult static func become(_ objectId: String,
copyEntireInstallation: Bool = true,
options: API.Options = []) async throws -> Self {
try await withCheckedThrowingContinuation { continuation in
Self.become(objectId,
copyEntireInstallation: copyEntireInstallation,
options: options,
completion: continuation.resume)
}
}
}

// MARK: Batch Support
public extension Sequence where Element: ParseInstallation {
/**
Fetches a collection of installations *aynchronously* with the current data from the server and sets
Expand Down Expand Up @@ -321,8 +349,8 @@ public extension ParseInstallation {
- warning: The latest **PFInstallation** from the Objective-C SDK should be saved to your
Parse Server before calling this method.
*/
@available(*, deprecated, message: "This does not work, use become() instead")
@discardableResult static func migrateFromObjCKeychain(copyEntireInstallation: Bool = true,
deleteObjectiveCKeychain: Bool = false,
options: API.Options = []) async throws -> Self {
try await withCheckedThrowingContinuation { continuation in
Self.migrateFromObjCKeychain(copyEntireInstallation: copyEntireInstallation,
Expand All @@ -338,9 +366,14 @@ public extension ParseInstallation {
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: Returns saved `ParseInstallation`.
- throws: An error of type `ParseError`.
- note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
desires a different policy, it should be inserted in `options`.
- warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false**
when calling this method.
- warning: It is recommended to only use this method after a succesfful migration. Calling this
method will destroy the entire Objective-C Keychain and `ParseInstallation` on the Parse
Server.
Server. This method assumes **PFInstallation.installationId** is saved to the Keychain. If the
**installationId** is not saved to the Keychain, this method will not work.
*/
static func deleteObjCKeychain(options: API.Options = []) async throws {
let result = try await withCheckedThrowingContinuation { continuation in
Expand Down
32 changes: 31 additions & 1 deletion Sources/ParseSwift/Objects/ParseInstallation+combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,36 @@ public extension ParseInstallation {
self.delete(options: options, completion: promise)
}
}

/**
Copies the `ParseInstallation` *asynchronously* based on the `installationId` and publishes
when complete. On success, this saves the `ParseInstallation` to the keychain, so you can retrieve
the current installation using *current*.

- parameter installationId: The **id** of the `ParseInstallation` to become.
- parameter copyEntireInstallation: When **true**, copies the entire `ParseInstallation`.
When **false**, only the `channels` and `deviceToken` are copied; resulting in a new
`ParseInstallation` for original `sessionToken`. Defaults to **true**.
- 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.
It should have the following argument signature: `(Result<Self, ParseError>)`.
- note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
desires a different policy, it should be inserted in `options`.
*/
static func becomePublisher(_ installationId: String,
copyEntireInstallation: Bool = true,
options: API.Options = []) -> Future<Self, ParseError> {
Future { promise in
Self.become(installationId,
copyEntireInstallation: copyEntireInstallation,
options: options,
completion: promise)
}
}
}

// MARK: Batch Support
public extension Sequence where Element: ParseInstallation {
/**
Fetches a collection of installations *aynchronously* with the current data from the server and sets
Expand Down Expand Up @@ -313,8 +341,10 @@ public extension ParseInstallation {
- warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false**
when calling this method.
- warning: The latest **PFInstallation** from the Objective-C SDK should be saved to your
Parse Server before calling this method.
Parse Server before calling this method. This method assumes **PFInstallation.installationId** is saved
to the Keychain. If the **installationId** is not saved to the Keychain, this method will not work.
*/
@available(*, deprecated, message: "This does not work, use become() instead")
static func migrateFromObjCKeychainPublisher(copyEntireInstallation: Bool = true,
options: API.Options = []) -> Future<Self, ParseError> {
Future { promise in
Expand Down
129 changes: 79 additions & 50 deletions Sources/ParseSwift/Objects/ParseInstallation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,76 @@ public extension ParseInstallation {
Self.updateInternalFieldsCorrectly()
}
}

/**
Copy the `ParseInstallation` *asynchronously* based on the `objectId`.
On success, this saves the `ParseInstallation` to the keychain, so you can retrieve
the current installation using *current*.

- parameter objectId: The **id** of the `ParseInstallation` to become.
- parameter copyEntireInstallation: When **true**, copies the entire `ParseInstallation`.
When **false**, only the `channels` and `deviceToken` are copied; resulting in a new
`ParseInstallation` for original `sessionToken`. Defaults to **true**.
- 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.
It should have the following argument signature: `(Result<Self, ParseError>)`.
- note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
desires a different policy, it should be inserted in `options`.
*/
static func become(_ objectId: String,
copyEntireInstallation: Bool = true,
options: API.Options = [],
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<Self, ParseError>) -> Void) {
guard var currentInstallation = Self.current else {
let error = ParseError(code: .unknownError,
message: "Current installation does not exist")
callbackQueue.async {
completion(.failure(error))
}
return
}
guard currentInstallation.objectId != objectId else {
// If the installationId's are the same, assume successful replacement already occured.
callbackQueue.async {
completion(.success(currentInstallation))
}
return
}
currentInstallation.objectId = objectId
currentInstallation.fetch(options: options, callbackQueue: callbackQueue) { result in
switch result {
case .success(var updatedInstallation):
if copyEntireInstallation {
updatedInstallation.updateAutomaticInfo()
Self.currentContainer.installationId = updatedInstallation.installationId
Self.currentContainer.currentInstallation = updatedInstallation
} else {
Self.current?.channels = updatedInstallation.channels
if Self.current?.deviceToken == nil {
Self.current?.deviceToken = updatedInstallation.deviceToken
}
}
Self.saveCurrentContainerToKeychain()
guard let latestInstallation = Self.current else {
let error = ParseError(code: .unknownError,
message: "Had trouble migrating the installation")
callbackQueue.async {
completion(.failure(error))
}
return
}
latestInstallation.save(options: options,
callbackQueue: callbackQueue,
completion: completion)
case .failure(let error):
callbackQueue.async {
completion(.failure(error))
}
}
}
}
}

// MARK: Automatic Info
Expand Down Expand Up @@ -1533,68 +1603,27 @@ public extension ParseInstallation {
- warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false**
when calling this method.
- warning: The latest **PFInstallation** from the Objective-C SDK should be saved to your
Parse Server before calling this method.
Parse Server before calling this method. This method assumes **PFInstallation.installationId**
is saved to the Keychain. If the **installationId** is not saved to the Keychain, this method will
not work.
*/
@available(*, deprecated, message: "This does not work, use become() instead")
static func migrateFromObjCKeychain(copyEntireInstallation: Bool = true,
options: API.Options = [],
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<Self, ParseError>) -> Void) {
guard let objcParseKeychain = KeychainStore.objectiveC,
let oldInstallationId: String = objcParseKeychain.object(forKey: "installationId") else {
let oldInstallationId: String = objcParseKeychain.objectObjectiveC(forKey: "installationId") else {
let error = ParseError(code: .unknownError,
message: "Could not find Installation in the Objective-C SDK Keychain")
callbackQueue.async {
completion(.failure(error))
}
return
}
guard var currentInstallation = Self.current else {
let error = ParseError(code: .unknownError,
message: "Current installation does not exist")
callbackQueue.async {
completion(.failure(error))
}
return
}
guard currentInstallation.installationId != oldInstallationId else {
// If the installationId's are the same, assume successful migration already occured.
callbackQueue.async {
completion(.success(currentInstallation))
}
return
}
currentInstallation.installationId = oldInstallationId
currentInstallation.fetch(options: options, callbackQueue: callbackQueue) { result in
switch result {
case .success(var updatedInstallation):
if copyEntireInstallation {
updatedInstallation.updateAutomaticInfo()
Self.currentContainer.installationId = updatedInstallation.installationId
Self.currentContainer.currentInstallation = updatedInstallation
} else {
Self.current?.channels = updatedInstallation.channels
if Self.current?.deviceToken == nil {
Self.current?.deviceToken = updatedInstallation.deviceToken
}
}
Self.saveCurrentContainerToKeychain()
guard let latestInstallation = Self.current else {
let error = ParseError(code: .unknownError,
message: "Had trouble migrating the installation")
callbackQueue.async {
completion(.failure(error))
}
return
}
latestInstallation.save(options: options,
callbackQueue: callbackQueue,
completion: completion)
case .failure(let error):
callbackQueue.async {
completion(.failure(error))
}
}
}
become(oldInstallationId,
copyEntireInstallation: copyEntireInstallation,
completion: completion)
}

/**
Expand All @@ -1616,7 +1645,7 @@ public extension ParseInstallation {
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<Void, ParseError>) -> Void) {
guard let objcParseKeychain = KeychainStore.objectiveC,
let oldInstallationId: String = objcParseKeychain.object(forKey: "installationId") else {
let oldInstallationId: String = objcParseKeychain.objectObjectiveC(forKey: "installationId") else {
let error = ParseError(code: .unknownError,
message: "Could not find Installation in the Objective-C SDK Keychain")
callbackQueue.async {
Expand Down
21 changes: 12 additions & 9 deletions Sources/ParseSwift/Objects/ParseUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,8 +263,9 @@ extension ParseUser {
}

/**
Logs in a `ParseUser` *synchronously* with a session token. On success, this saves the session
to the keychain, so you can retrieve the currently logged in user using *current*.
Logs in a `ParseUser` *synchronously* with a session token. On success, this saves the logged in
`ParseUser`with this session to the keychain, so you can retrieve the currently logged in user using
*current*.

- parameter sessionToken: The sessionToken of the user to login.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
Expand All @@ -283,8 +284,9 @@ extension ParseUser {
}

/**
Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the session
to the keychain, so you can retrieve the currently logged in user using *current*.
Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the logged in
`ParseUser`with this session to the keychain, so you can retrieve the currently logged in user using
*current*.

- parameter sessionToken: The sessionToken of the user to login.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
Expand All @@ -306,8 +308,9 @@ extension ParseUser {
}

/**
Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the session
to the keychain, so you can retrieve the currently logged in user using *current*.
Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the logged in
`ParseUser`with this session to the keychain, so you can retrieve the currently logged in user using
*current*.

- parameter sessionToken: The sessionToken of the user to login.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
Expand Down Expand Up @@ -372,10 +375,10 @@ extension ParseUser {
completion: @escaping (Result<Self, ParseError>) -> Void) {

let objcParseKeychain = KeychainStore.objectiveC
let objcParseSessionToken: String? = objcParseKeychain?.object(forKey: "sessionToken") ??
objcParseKeychain?.object(forKey: "session_token")

guard let sessionToken = objcParseSessionToken else {
guard let objcParseUser: [String: String] = objcParseKeychain?.objectObjectiveC(forKey: "currentUser"),
let sessionToken: String = objcParseUser["sessionToken"] ??
objcParseUser["session_token"] else {
let error = ParseError(code: .unknownError,
message: "Could not find a session token in the Parse Objective-C SDK Keychain.")
callbackQueue.async {
Expand Down
4 changes: 2 additions & 2 deletions Sources/ParseSwift/Parse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public func initialize(configuration: ParseConfiguration) {
#if !os(Linux) && !os(Android) && !os(Windows)
if configuration.isMigratingFromObjcSDK {
if let objcParseKeychain = KeychainStore.objectiveC {
guard let installationId: String = objcParseKeychain.object(forKey: "installationId"),
guard let installationId: String = objcParseKeychain.objectObjectiveC(forKey: "installationId"),
BaseParseInstallation.current?.installationId != installationId else {
return
}
Expand Down Expand Up @@ -383,7 +383,7 @@ public func clearCache() {
- warning: The keychain cannot be recovered after deletion.
*/
public func deleteObjectiveCKeychain() throws {
try KeychainStore.objectiveC?.deleteAll()
try KeychainStore.objectiveC?.deleteAllObjectiveC()
}

/**
Expand Down
Loading