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