1 // Copyright 2015-present 650 Industries. All rights reserved.
2 
3 import Foundation
4 
5 /**
6  * The build data stored by the configuration is subject to change when
7  * a user updates the binary.
8  *
9  * This can lead to inconsistent update loading behavior, for
10  * example: https://github.com/expo/expo/issues/14372
11  *
12  * This class wipes the updates when any of the tracked build data
13  * changes. This leaves the user in the same situation as a fresh install.
14  *
15  * So far we only know that `releaseChannel` and
16  * `requestHeaders[expo-channel-name]` are dangerous to change, but have
17  * included a few more that both seem unlikely to change (so we clear
18  * the updates cache rarely) and likely to
19  * cause bugs when they do. The tracked fields are:
20  *
21  *   EXUpdatesReleaseChannel
22  *   EXUpdatesURL
23  *
24  * and all of the values in json
25  *
26  *   EXUpdatesRequestHeaders
27  */
28 internal final class UpdatesBuildData {
ensureBuildDataIsConsistentAsyncnull29   static func ensureBuildDataIsConsistentAsync(database: UpdatesDatabase, config: UpdatesConfig) {
30     database.databaseQueue.async {
31       guard let scopeKey = config.scopeKey else {
32         NSException(
33           name: .internalInconsistencyException,
34           reason: "expo-updates was configured with no scope key. Make sure a valid URL is configured under EXUpdatesURL."
35         ).raise()
36         return
37       }
38 
39       let staticBuildData: [AnyHashable: Any]?
40       do {
41         staticBuildData = try database.staticBuildData(withScopeKey: scopeKey)
42       } catch {
43         NSLog("Error getting static build data: %@", [error.localizedDescription])
44         return
45       }
46 
47       if let staticBuildData = staticBuildData {
48         let impliedStaticBuildData = self.getBuildDataFromConfig(config)
49         // safest dictionary comparison conversion is still in objective-c
50         // swiftlint:disable:next legacy_objc_type
51         if !NSDictionary(dictionary: staticBuildData).isEqual(to: impliedStaticBuildData) {
52           clearAllUpdatesAndSetStaticBuildData(database: database, config: config, scopeKey: scopeKey)
53         }
54       } else {
55         do {
56           try database.setStaticBuildData(getBuildDataFromConfig(config), withScopeKey: scopeKey)
57         } catch {
58           NSLog("Error setting static build data: %@", [error.localizedDescription])
59           return
60         }
61       }
62     }
63   }
64 
getBuildDataFromConfignull65   static func getBuildDataFromConfig(_ config: UpdatesConfig) -> [String: Any] {
66     return [
67       "EXUpdatesURL": config.updateUrl.require("Must supply updateUrl in config").absoluteString,
68       "EXUpdatesReleaseChannel": config.releaseChannel,
69       "EXUpdatesRequestHeaders": config.requestHeaders
70     ]
71   }
72 
clearAllUpdatesAndSetStaticBuildDatanull73   static func clearAllUpdatesAndSetStaticBuildData(database: UpdatesDatabase, config: UpdatesConfig, scopeKey: String) {
74     let allUpdates: [Update]
75     do {
76       allUpdates = try database.allUpdates(withConfig: config)
77     } catch {
78       NSLog("Error loading updates from database: %@", [error.localizedDescription])
79       return
80     }
81 
82     do {
83       try database.deleteUpdates(allUpdates)
84     } catch {
85       NSLog("Error clearing all updates from database: %@", [error.localizedDescription])
86       return
87     }
88 
89     do {
90       try database.setStaticBuildData(getBuildDataFromConfig(config), withScopeKey: scopeKey)
91     } catch {
92       NSLog("Error setting static build data: %@", [error.localizedDescription])
93       return
94     }
95   }
96 }
97