Skip to content

Commit 62553bc

Browse files
authored
feat: add a copy installation method and fix loginUsingObjCKeychain (#407)
* fix: login with ObjC keychain * add changelog * add become method to installation * pass copyEntireInstallation to become * fix method labels * fix querying objc keychain * fix installation become * become uses fetch objectId * fix linux tests * add test for keychain * delete keychain after each test
1 parent a5dbff4 commit 62553bc

13 files changed

+672
-325
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.10.0...main)
55
* _Contributing to this repo? Add info about your change here to be included in the next release_
66

7+
__New features__
8+
- 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).
9+
10+
__Fixes__
11+
- 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).
12+
713
### 4.11.0
814
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.10.0...4.11.0)
915

Sources/ParseSwift/Objects/ParseInstallation+async.swift

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,36 @@ public extension ParseInstallation {
120120
throw error
121121
}
122122
}
123+
124+
/**
125+
Copy the `ParseInstallation` *asynchronously* based on the `objectId`.
126+
On success, this saves the `ParseInstallation` to the keychain, so you can retrieve
127+
the current installation using *current*.
128+
129+
- parameter objectId: The **id** of the `ParseInstallation` to become.
130+
- parameter copyEntireInstallation: When **true**, copies the entire `ParseInstallation`.
131+
When **false**, only the `channels` and `deviceToken` are copied; resulting in a new
132+
`ParseInstallation` for original `sessionToken`. Defaults to **true**.
133+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
134+
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
135+
- parameter completion: The block to execute.
136+
It should have the following argument signature: `(Result<Self, ParseError>)`.
137+
- note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
138+
desires a different policy, it should be inserted in `options`.
139+
*/
140+
@discardableResult static func become(_ objectId: String,
141+
copyEntireInstallation: Bool = true,
142+
options: API.Options = []) async throws -> Self {
143+
try await withCheckedThrowingContinuation { continuation in
144+
Self.become(objectId,
145+
copyEntireInstallation: copyEntireInstallation,
146+
options: options,
147+
completion: continuation.resume)
148+
}
149+
}
123150
}
124151

152+
// MARK: Batch Support
125153
public extension Sequence where Element: ParseInstallation {
126154
/**
127155
Fetches a collection of installations *aynchronously* with the current data from the server and sets
@@ -321,8 +349,8 @@ public extension ParseInstallation {
321349
- warning: The latest **PFInstallation** from the Objective-C SDK should be saved to your
322350
Parse Server before calling this method.
323351
*/
352+
@available(*, deprecated, message: "This does not work, use become() instead")
324353
@discardableResult static func migrateFromObjCKeychain(copyEntireInstallation: Bool = true,
325-
deleteObjectiveCKeychain: Bool = false,
326354
options: API.Options = []) async throws -> Self {
327355
try await withCheckedThrowingContinuation { continuation in
328356
Self.migrateFromObjCKeychain(copyEntireInstallation: copyEntireInstallation,
@@ -338,9 +366,14 @@ public extension ParseInstallation {
338366
- parameter options: A set of header options sent to the server. Defaults to an empty set.
339367
- returns: Returns saved `ParseInstallation`.
340368
- throws: An error of type `ParseError`.
369+
- note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
370+
desires a different policy, it should be inserted in `options`.
371+
- warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false**
372+
when calling this method.
341373
- warning: It is recommended to only use this method after a succesfful migration. Calling this
342374
method will destroy the entire Objective-C Keychain and `ParseInstallation` on the Parse
343-
Server.
375+
Server. This method assumes **PFInstallation.installationId** is saved to the Keychain. If the
376+
**installationId** is not saved to the Keychain, this method will not work.
344377
*/
345378
static func deleteObjCKeychain(options: API.Options = []) async throws {
346379
let result = try await withCheckedThrowingContinuation { continuation in

Sources/ParseSwift/Objects/ParseInstallation+combine.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,36 @@ public extension ParseInstallation {
117117
self.delete(options: options, completion: promise)
118118
}
119119
}
120+
121+
/**
122+
Copies the `ParseInstallation` *asynchronously* based on the `installationId` and publishes
123+
when complete. On success, this saves the `ParseInstallation` to the keychain, so you can retrieve
124+
the current installation using *current*.
125+
126+
- parameter installationId: The **id** of the `ParseInstallation` to become.
127+
- parameter copyEntireInstallation: When **true**, copies the entire `ParseInstallation`.
128+
When **false**, only the `channels` and `deviceToken` are copied; resulting in a new
129+
`ParseInstallation` for original `sessionToken`. Defaults to **true**.
130+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
131+
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
132+
- parameter completion: The block to execute.
133+
It should have the following argument signature: `(Result<Self, ParseError>)`.
134+
- note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
135+
desires a different policy, it should be inserted in `options`.
136+
*/
137+
static func becomePublisher(_ installationId: String,
138+
copyEntireInstallation: Bool = true,
139+
options: API.Options = []) -> Future<Self, ParseError> {
140+
Future { promise in
141+
Self.become(installationId,
142+
copyEntireInstallation: copyEntireInstallation,
143+
options: options,
144+
completion: promise)
145+
}
146+
}
120147
}
121148

149+
// MARK: Batch Support
122150
public extension Sequence where Element: ParseInstallation {
123151
/**
124152
Fetches a collection of installations *aynchronously* with the current data from the server and sets
@@ -313,8 +341,10 @@ public extension ParseInstallation {
313341
- warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false**
314342
when calling this method.
315343
- warning: The latest **PFInstallation** from the Objective-C SDK should be saved to your
316-
Parse Server before calling this method.
344+
Parse Server before calling this method. This method assumes **PFInstallation.installationId** is saved
345+
to the Keychain. If the **installationId** is not saved to the Keychain, this method will not work.
317346
*/
347+
@available(*, deprecated, message: "This does not work, use become() instead")
318348
static func migrateFromObjCKeychainPublisher(copyEntireInstallation: Bool = true,
319349
options: API.Options = []) -> Future<Self, ParseError> {
320350
Future { promise in

Sources/ParseSwift/Objects/ParseInstallation.swift

Lines changed: 79 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,76 @@ public extension ParseInstallation {
303303
Self.updateInternalFieldsCorrectly()
304304
}
305305
}
306+
307+
/**
308+
Copy the `ParseInstallation` *asynchronously* based on the `objectId`.
309+
On success, this saves the `ParseInstallation` to the keychain, so you can retrieve
310+
the current installation using *current*.
311+
312+
- parameter objectId: The **id** of the `ParseInstallation` to become.
313+
- parameter copyEntireInstallation: When **true**, copies the entire `ParseInstallation`.
314+
When **false**, only the `channels` and `deviceToken` are copied; resulting in a new
315+
`ParseInstallation` for original `sessionToken`. Defaults to **true**.
316+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
317+
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
318+
- parameter completion: The block to execute.
319+
It should have the following argument signature: `(Result<Self, ParseError>)`.
320+
- note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
321+
desires a different policy, it should be inserted in `options`.
322+
*/
323+
static func become(_ objectId: String,
324+
copyEntireInstallation: Bool = true,
325+
options: API.Options = [],
326+
callbackQueue: DispatchQueue = .main,
327+
completion: @escaping (Result<Self, ParseError>) -> Void) {
328+
guard var currentInstallation = Self.current else {
329+
let error = ParseError(code: .unknownError,
330+
message: "Current installation does not exist")
331+
callbackQueue.async {
332+
completion(.failure(error))
333+
}
334+
return
335+
}
336+
guard currentInstallation.objectId != objectId else {
337+
// If the installationId's are the same, assume successful replacement already occured.
338+
callbackQueue.async {
339+
completion(.success(currentInstallation))
340+
}
341+
return
342+
}
343+
currentInstallation.objectId = objectId
344+
currentInstallation.fetch(options: options, callbackQueue: callbackQueue) { result in
345+
switch result {
346+
case .success(var updatedInstallation):
347+
if copyEntireInstallation {
348+
updatedInstallation.updateAutomaticInfo()
349+
Self.currentContainer.installationId = updatedInstallation.installationId
350+
Self.currentContainer.currentInstallation = updatedInstallation
351+
} else {
352+
Self.current?.channels = updatedInstallation.channels
353+
if Self.current?.deviceToken == nil {
354+
Self.current?.deviceToken = updatedInstallation.deviceToken
355+
}
356+
}
357+
Self.saveCurrentContainerToKeychain()
358+
guard let latestInstallation = Self.current else {
359+
let error = ParseError(code: .unknownError,
360+
message: "Had trouble migrating the installation")
361+
callbackQueue.async {
362+
completion(.failure(error))
363+
}
364+
return
365+
}
366+
latestInstallation.save(options: options,
367+
callbackQueue: callbackQueue,
368+
completion: completion)
369+
case .failure(let error):
370+
callbackQueue.async {
371+
completion(.failure(error))
372+
}
373+
}
374+
}
375+
}
306376
}
307377

308378
// MARK: Automatic Info
@@ -1533,68 +1603,27 @@ public extension ParseInstallation {
15331603
- warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false**
15341604
when calling this method.
15351605
- warning: The latest **PFInstallation** from the Objective-C SDK should be saved to your
1536-
Parse Server before calling this method.
1606+
Parse Server before calling this method. This method assumes **PFInstallation.installationId**
1607+
is saved to the Keychain. If the **installationId** is not saved to the Keychain, this method will
1608+
not work.
15371609
*/
1610+
@available(*, deprecated, message: "This does not work, use become() instead")
15381611
static func migrateFromObjCKeychain(copyEntireInstallation: Bool = true,
15391612
options: API.Options = [],
15401613
callbackQueue: DispatchQueue = .main,
15411614
completion: @escaping (Result<Self, ParseError>) -> Void) {
15421615
guard let objcParseKeychain = KeychainStore.objectiveC,
1543-
let oldInstallationId: String = objcParseKeychain.object(forKey: "installationId") else {
1616+
let oldInstallationId: String = objcParseKeychain.objectObjectiveC(forKey: "installationId") else {
15441617
let error = ParseError(code: .unknownError,
15451618
message: "Could not find Installation in the Objective-C SDK Keychain")
15461619
callbackQueue.async {
15471620
completion(.failure(error))
15481621
}
15491622
return
15501623
}
1551-
guard var currentInstallation = Self.current else {
1552-
let error = ParseError(code: .unknownError,
1553-
message: "Current installation does not exist")
1554-
callbackQueue.async {
1555-
completion(.failure(error))
1556-
}
1557-
return
1558-
}
1559-
guard currentInstallation.installationId != oldInstallationId else {
1560-
// If the installationId's are the same, assume successful migration already occured.
1561-
callbackQueue.async {
1562-
completion(.success(currentInstallation))
1563-
}
1564-
return
1565-
}
1566-
currentInstallation.installationId = oldInstallationId
1567-
currentInstallation.fetch(options: options, callbackQueue: callbackQueue) { result in
1568-
switch result {
1569-
case .success(var updatedInstallation):
1570-
if copyEntireInstallation {
1571-
updatedInstallation.updateAutomaticInfo()
1572-
Self.currentContainer.installationId = updatedInstallation.installationId
1573-
Self.currentContainer.currentInstallation = updatedInstallation
1574-
} else {
1575-
Self.current?.channels = updatedInstallation.channels
1576-
if Self.current?.deviceToken == nil {
1577-
Self.current?.deviceToken = updatedInstallation.deviceToken
1578-
}
1579-
}
1580-
Self.saveCurrentContainerToKeychain()
1581-
guard let latestInstallation = Self.current else {
1582-
let error = ParseError(code: .unknownError,
1583-
message: "Had trouble migrating the installation")
1584-
callbackQueue.async {
1585-
completion(.failure(error))
1586-
}
1587-
return
1588-
}
1589-
latestInstallation.save(options: options,
1590-
callbackQueue: callbackQueue,
1591-
completion: completion)
1592-
case .failure(let error):
1593-
callbackQueue.async {
1594-
completion(.failure(error))
1595-
}
1596-
}
1597-
}
1624+
become(oldInstallationId,
1625+
copyEntireInstallation: copyEntireInstallation,
1626+
completion: completion)
15981627
}
15991628

16001629
/**
@@ -1616,7 +1645,7 @@ public extension ParseInstallation {
16161645
callbackQueue: DispatchQueue = .main,
16171646
completion: @escaping (Result<Void, ParseError>) -> Void) {
16181647
guard let objcParseKeychain = KeychainStore.objectiveC,
1619-
let oldInstallationId: String = objcParseKeychain.object(forKey: "installationId") else {
1648+
let oldInstallationId: String = objcParseKeychain.objectObjectiveC(forKey: "installationId") else {
16201649
let error = ParseError(code: .unknownError,
16211650
message: "Could not find Installation in the Objective-C SDK Keychain")
16221651
callbackQueue.async {

Sources/ParseSwift/Objects/ParseUser.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,9 @@ extension ParseUser {
263263
}
264264

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

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

285286
/**
286-
Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the session
287-
to the keychain, so you can retrieve the currently logged in user using *current*.
287+
Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the logged in
288+
`ParseUser`with this session to the keychain, so you can retrieve the currently logged in user using
289+
*current*.
288290

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

308310
/**
309-
Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the session
310-
to the keychain, so you can retrieve the currently logged in user using *current*.
311+
Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the logged in
312+
`ParseUser`with this session to the keychain, so you can retrieve the currently logged in user using
313+
*current*.
311314

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

374377
let objcParseKeychain = KeychainStore.objectiveC
375-
let objcParseSessionToken: String? = objcParseKeychain?.object(forKey: "sessionToken") ??
376-
objcParseKeychain?.object(forKey: "session_token")
377378

378-
guard let sessionToken = objcParseSessionToken else {
379+
guard let objcParseUser: [String: String] = objcParseKeychain?.objectObjectiveC(forKey: "currentUser"),
380+
let sessionToken: String = objcParseUser["sessionToken"] ??
381+
objcParseUser["session_token"] else {
379382
let error = ParseError(code: .unknownError,
380383
message: "Could not find a session token in the Parse Objective-C SDK Keychain.")
381384
callbackQueue.async {

Sources/ParseSwift/Parse.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public func initialize(configuration: ParseConfiguration) {
160160
#if !os(Linux) && !os(Android) && !os(Windows)
161161
if configuration.isMigratingFromObjcSDK {
162162
if let objcParseKeychain = KeychainStore.objectiveC {
163-
guard let installationId: String = objcParseKeychain.object(forKey: "installationId"),
163+
guard let installationId: String = objcParseKeychain.objectObjectiveC(forKey: "installationId"),
164164
BaseParseInstallation.current?.installationId != installationId else {
165165
return
166166
}
@@ -383,7 +383,7 @@ public func clearCache() {
383383
- warning: The keychain cannot be recovered after deletion.
384384
*/
385385
public func deleteObjectiveCKeychain() throws {
386-
try KeychainStore.objectiveC?.deleteAll()
386+
try KeychainStore.objectiveC?.deleteAllObjectiveC()
387387
}
388388

389389
/**

0 commit comments

Comments
 (0)