1 package expo.modules.manifests.core
2 
3 import expo.modules.jsonutils.getNullable
4 import expo.modules.jsonutils.require
5 import org.json.JSONArray
6 import org.json.JSONException
7 import org.json.JSONObject
8 
9 interface InternalJSONMutator {
10   @Throws(JSONException::class)
updateJSONnull11   fun updateJSON(json: JSONObject)
12 }
13 
14 abstract class Manifest(protected val json: JSONObject) {
15   @Deprecated(message = "Strive for manifests to be immutable")
16   @Throws(JSONException::class)
17   fun mutateInternalJSONInPlace(internalJSONMutator: InternalJSONMutator) {
18     json.apply {
19       internalJSONMutator.updateJSON(this)
20     }
21   }
22 
23   @Deprecated(message = "Prefer to use specific field getters")
24   fun getRawJson(): JSONObject = json
25 
26   @Deprecated(message = "Prefer to use specific field getters")
27   override fun toString(): String {
28     return getRawJson().toString()
29   }
30 
31   /**
32    * A best-effort immutable legacy ID for this experience. Stable through project transfers.
33    * Should be used for calling Expo and EAS APIs during their transition to projectId.
34    */
35   @Deprecated(message = "Prefer scopeKey or projectId depending on use case")
36   abstract fun getStableLegacyID(): String?
37 
38   /**
39    * A stable immutable scoping key for this experience. Should be used for scoping data on the
40    * client for this project when running in Expo Go.
41    */
42   @Throws(JSONException::class)
43   abstract fun getScopeKey(): String
44 
45   /**
46    * A stable UUID for this EAS project. Should be used to call EAS APIs.
47    */
48   abstract fun getEASProjectID(): String?
49 
50   /**
51    * The legacy ID of this experience.
52    * - For Bare manifests, formatted as a UUID.
53    * - For Legacy manifests, formatted as @owner/slug. Not stable through project transfers.
54    * - For New manifests, currently incorrect value is UUID.
55    *
56    * Use this in cases where an identifier of the current manifest is needed (experience loading for example).
57    * Use getScopeKey for cases where a stable key is needed to scope data to this experience.
58    * Use getEASProjectID for cases where a stable UUID identifier of the experience is needed to identify over EAS APIs.
59    * Use getStableLegacyID for cases where a stable legacy format identifier of the experience is needed (experience scoping for example).
60    */
61   @Throws(JSONException::class)
62   @Deprecated(message = "Prefer scopeKey or projectId depending on use case")
63   fun getLegacyID(): String = json.require("id")
64 
65   @Throws(JSONException::class)
66   abstract fun getBundleURL(): String
67 
68   @Throws(JSONException::class)
69   fun getRevisionId(): String = getExpoClientConfigRootObject()!!.require("revisionId")
70 
71   fun getMetadata(): JSONObject? = json.getNullable("metadata")
72 
73   /**
74    * Get the SDK version that should be attempted to be used in Expo Go. If no SDK version can be
75    * determined, returns null
76    */
77   abstract fun getExpoGoSDKVersion(): String?
78 
79   abstract fun getAssets(): JSONArray?
80 
81   abstract fun getExpoGoConfigRootObject(): JSONObject?
82   abstract fun getExpoClientConfigRootObject(): JSONObject?
83 
84   fun isDevelopmentMode(): Boolean {
85     val expoGoRootObject = getExpoGoConfigRootObject() ?: return false
86     return try {
87       expoGoRootObject.has("developer") &&
88         expoGoRootObject.getNullable<JSONObject>("packagerOpts")?.getNullable("dev") ?: false
89     } catch (e: JSONException) {
90       false
91     }
92   }
93 
94   fun isDevelopmentSilentLaunch(): Boolean {
95     val expoGoRootObject = getExpoGoConfigRootObject() ?: return false
96     return expoGoRootObject.getNullable<JSONObject>("developmentClient")?.getNullable("silentLaunch") ?: false
97   }
98 
99   fun isUsingDeveloperTool(): Boolean {
100     val expoGoRootObject = getExpoGoConfigRootObject() ?: return false
101     return expoGoRootObject.getNullable<JSONObject>("developer")?.has("tool") ?: false
102   }
103 
104   abstract fun getSlug(): String?
105 
106   fun getDebuggerHost(): String = getExpoGoConfigRootObject()!!.require("debuggerHost")
107   fun getMainModuleName(): String = getExpoGoConfigRootObject()!!.require("mainModuleName")
108   fun getHostUri(): String? = getExpoClientConfigRootObject()?.getNullable("hostUri")
109 
110   fun isVerified(): Boolean = json.getNullable("isVerified") ?: false
111 
112   abstract fun getAppKey(): String?
113 
114   fun getName(): String? {
115     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
116     return expoClientConfig.getNullable("name")
117   }
118 
119   fun getVersion(): String? {
120     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
121     return expoClientConfig.getNullable("version")
122   }
123 
124   fun getUpdatesInfo(): JSONObject? {
125     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
126     return expoClientConfig.getNullable("updates")
127   }
128 
129   fun getPrimaryColor(): String? {
130     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
131     return expoClientConfig.getNullable("primaryColor")
132   }
133 
134   fun getOrientation(): String? {
135     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
136     return expoClientConfig.getNullable("orientation")
137   }
138 
139   fun getAndroidKeyboardLayoutMode(): String? {
140     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
141     val android = expoClientConfig.getNullable<JSONObject>("android") ?: return null
142     return android.getNullable("softwareKeyboardLayoutMode")
143   }
144 
145   fun getAndroidUserInterfaceStyle(): String? {
146     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
147     return try {
148       expoClientConfig.require<JSONObject>("android").require("userInterfaceStyle")
149     } catch (e: JSONException) {
150       expoClientConfig.getNullable("userInterfaceStyle")
151     }
152   }
153 
154   fun getAndroidStatusBarOptions(): JSONObject? {
155     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
156     return expoClientConfig.getNullable("androidStatusBar")
157   }
158 
159   fun getAndroidBackgroundColor(): String? {
160     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
161     return try {
162       expoClientConfig.require<JSONObject>("android").require("backgroundColor")
163     } catch (e: JSONException) {
164       expoClientConfig.getNullable("backgroundColor")
165     }
166   }
167 
168   fun getAndroidNavigationBarOptions(): JSONObject? {
169     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
170     return expoClientConfig.getNullable("androidNavigationBar")
171   }
172 
173   val jsEngine: String by lazy {
174     val expoClientConfig = getExpoClientConfigRootObject()
175     var result = expoClientConfig
176       ?.getNullable<JSONObject>("android")?.getNullable<String>("jsEngine") ?: expoClientConfig?.getNullable<String>("jsEngine")
177     if (result == null) {
178       val sdkVersionComponents = getExpoGoSDKVersion()?.split(".")
179       val sdkMajorVersion = if (sdkVersionComponents?.size == 3) sdkVersionComponents[0].toIntOrNull() else 0
180       result = if (sdkMajorVersion in 1..47) "jsc" else "hermes"
181     }
182     result
183   }
184 
185   fun getIconUrl(): String? {
186     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
187     return expoClientConfig.getNullable("iconUrl")
188   }
189 
190   fun getNotificationPreferences(): JSONObject? {
191     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
192     return expoClientConfig.getNullable("notification")
193   }
194 
195   fun getAndroidSplashInfo(): JSONObject? {
196     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
197     return expoClientConfig.getNullable<JSONObject>("android")?.getNullable("splash")
198   }
199 
200   fun getRootSplashInfo(): JSONObject? {
201     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
202     return expoClientConfig.getNullable("splash")
203   }
204 
205   fun getAndroidGoogleServicesFile(): String? {
206     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
207     val android = expoClientConfig.getNullable<JSONObject>("android") ?: return null
208     return android.getNullable("googleServicesFile")
209   }
210 
211   fun getAndroidPackageName(): String? {
212     val expoClientConfig = getExpoClientConfigRootObject() ?: return null
213     val android = expoClientConfig.getNullable<JSONObject>("android") ?: return null
214     return android.getNullable("packageName")
215   }
216 
217   fun shouldUseNextNotificationsApi(): Boolean {
218     val expoClientConfig = getExpoClientConfigRootObject() ?: return false
219     val android: JSONObject = expoClientConfig.getNullable<JSONObject>("android") ?: return false
220     return android.getNullable("useNextNotificationsApi") ?: false
221   }
222 
223   @Throws(JSONException::class)
224   fun getFacebookAppId(): String = getExpoClientConfigRootObject()!!.require("facebookAppId")
225 
226   @Throws(JSONException::class)
227   fun getFacebookApplicationName(): String = getExpoClientConfigRootObject()!!.require("facebookDisplayName")
228 
229   @Throws(JSONException::class)
230   fun getFacebookAutoInitEnabled(): Boolean = getExpoClientConfigRootObject()!!.require("facebookAutoInitEnabled")
231 
232   /**
233    * Queries the dedicated package properties in `plugins`
234    */
235   @Throws(JSONException::class, IllegalArgumentException::class)
236   fun getPluginProperties(packageName: String): Map<String, Any>? {
237     val pluginsRawValue = getExpoClientConfigRootObject()?.getNullable<JSONArray>("plugins") ?: return null
238     val plugins = PluginType.fromRawArrayValue(pluginsRawValue) ?: return null
239     return plugins.filterIsInstance<PluginType.WithProps>()
240       .firstOrNull { it.plugin.first == packageName }
241       ?.plugin?.second
242   }
243 
244   companion object {
245     @JvmStatic fun fromManifestJson(manifestJson: JSONObject): Manifest {
246       return when {
247         manifestJson.has("releaseId") -> {
248           LegacyManifest(manifestJson)
249         }
250         manifestJson.has("metadata") -> {
251           NewManifest(manifestJson)
252         }
253         else -> {
254           BareManifest(manifestJson)
255         }
256       }
257     }
258   }
259 }
260 
261 internal typealias PluginWithProps = Pair<String, Map<String, Any>>
262 internal typealias PluginWithoutProps = String
263 internal sealed class PluginType {
264   data class WithProps(val plugin: PluginWithProps) : PluginType()
265   data class WithoutProps(val plugin: PluginWithoutProps) : PluginType()
266 
267   companion object {
268     @Throws(IllegalArgumentException::class)
fromRawValuenull269     private fun fromRawValue(value: Any): PluginType? {
270       return when (value) {
271         is JSONArray -> {
272           if (value.length() == 0) {
273             throw IllegalArgumentException("Value for (key = plugins) has incorrect type")
274           }
275           val name = value.get(0) as? String ?: return null
276           when (value.length()) {
277             2 -> {
278               val props = value.get(1) as? JSONObject ?: return null
279               WithProps(name to props.toMap())
280             }
281             else -> {
282               WithoutProps(name)
283             }
284           }
285         }
286         is String -> {
287           WithoutProps(value)
288         }
289         else -> throw IllegalArgumentException("Value for (key = plugins) has incorrect type")
290       }
291     }
292 
293     @Throws(IllegalArgumentException::class)
fromRawArrayValuenull294     fun fromRawArrayValue(value: JSONArray): List<PluginType> {
295       return mutableListOf<PluginType>().apply {
296         for (i in 0 until value.length()) {
297           fromRawValue(value.get(i))?.let {
298             add(it)
299           }
300         }
301       }
302     }
303   }
304 }
305