From 6d4228cc4438f38d1ee9f07fc190ff05a76cd99a Mon Sep 17 00:00:00 2001 From: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:25:00 -0700 Subject: [PATCH] fix(mobile): require exact activation metadata --- ...hybrid-webview-implementation-checklist.md | 15 ++-- ...-mobile-hybrid-webview-parity-inventory.md | 5 +- ...bile-hybrid-webview-single-pr-migration.md | 12 ++-- ...27-mobile-hybrid-webview-remaining-work.md | 13 +++- .../MobileWebActivationMetadata.kt | 40 +++++++++++ .../mobilewebshell/MobileWebPackageStore.kt | 14 +--- .../MobileWebActivationMetadataTest.kt | 42 +++++++++++ .../MobileWebPackageStoreTest.kt | 21 ------ .../MobileWebActivationMetadataTests.swift | 69 +++++++++++++++++++ .../MobileWebPackageStoreTests.swift | 29 +------- .../ios/MobileWebActivationMetadata.swift | 31 +++++++++ .../ios/MobileWebPackageStore.swift | 11 +-- .../run-ios-mobile-web-store-tests.mjs | 5 ++ 13 files changed, 224 insertions(+), 83 deletions(-) create mode 100644 mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebActivationMetadata.kt create mode 100644 mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebActivationMetadataTest.kt create mode 100644 mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebActivationMetadataTests.swift create mode 100644 mobile/packages/expo-mobile-web-shell/ios/MobileWebActivationMetadata.swift diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md index f0eba5e1608..4c4c8f995f8 100644 --- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md +++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-implementation-checklist.md @@ -1548,10 +1548,13 @@ copy. type-confused, noncanonical, length-mismatched, and extra-field responses. Swift and Kotlin enforce the same encoded ceiling before base64 decoding. They also cap each raw manifest document at 256 KiB before JSON parsing. - Native activation records also require exact string-typed active/previous - hashes; numeric hash-shaped values fail with the same stable error on iOS and - Android. Manifest and package-RPC schemas now share one exact asset-path - predicate. A mirrored TypeScript, Swift, and Kotlin corpus rejects empty, + Native activation records now accept only an exact object containing + string-typed `active` and optional distinct `previous` hashes. The mirrored + Swift/Kotlin corpus rejects missing, null, Boolean, numeric, array, uppercase, + duplicate-generation, unknown-field, and trailing-token mutations with + `mobile_web_activation_invalid`. Manifest and package-RPC schemas now share + one exact asset-path predicate. A mirrored TypeScript, Swift, and Kotlin + corpus rejects empty, absolute, traversal, repeated-separator, percent-encoded, query, fragment, backslash, non-ASCII, overlong, and trailing-newline paths while accepting only the reviewed relative form. Shared application SHA-256, Git object ID, @@ -1570,7 +1573,7 @@ copy. activation metadata at most 1 KiB plus one, and assets at most their declared length plus one on both platforms. Mirrored Swift/Kotlin faults preserve the stable generation and activation errors. Broader generated mutation, CSP - behavior, and cache metadata fuzzing remains open. + behavior, and generation-directory cache fuzzing remains open. - [ ] Fuzz bridge envelopes, schemas, sizes, IDs, ordering, cancellation, and subscription lifecycle. - [ ] Attempt cross-host, cross-build, cross-workspace, and cross-session races. @@ -2643,4 +2646,6 @@ and diff hygiene pass. A fresh production RNW build remains | 2026-07-28 | Complete | The first production bridge release policy is frozen at exact v2. Production packaging imports the shared range, additive features stay on v2 through capability negotiation, and a future floor cannot retire until its replacement has shipped in at least two stable mobile releases and the supported shell minimum advances. | | 2026-07-28 | Complete | The production rollback runbook now distinguishes bad Desktop packages from native-shell/store incidents, maps automatic and manual host-scoped recovery, requires corrected release artifacts rather than cache mutation, defines privacy-safe support evidence, and preserves final physical/store-signed drills as open release gates. | | 2026-07-28 | Complete | Post-runbook validation passes 569 mobile files / 3,378 tests with 2 expected skips. Mobile and mobile-web typechecks/lints, reliability, max-lines, formatting, diff hygiene, and the unchanged `b17ead7a…` 49-asset package verification pass. | +| 2026-07-28 | Finding | Swift `Decodable` ignored unknown activation fields, both stores admitted an explicit null or identical previous generation, and Android `JSONObject` accepted a valid activation object followed by trailing tokens. Both native stores now consume one exact activation object with only distinct lowercase string hashes. | +| 2026-07-28 | Complete | The mirrored two-valid/eleven-invalid activation corpus passes the iOS native fault executable and Android Debug unit/Release Kotlin gates. Full mobile validation remains 569 files / 3,378 tests with 2 expected skips; typechecks, lints, reliability, max-lines, formatting, diff hygiene, and unchanged `b17ead7a…` package verification pass. | | 2026-07-28 | Next | Complete the remaining gated cutover cleanup, then execute the physical-device, topology, security, performance, packaged-release, and App Store gates. | diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md index 1189ebedbb0..77d6af2b625 100644 --- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md +++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-parity-inventory.md @@ -521,7 +521,10 @@ Persisted primary and canonical manifests now use 256 KiB bounded readers, activation records use a 1 KiB bounded reader, and assets use their declared length on both native platforms. Each path reads only one overflow byte before failing with its stable error, and mirrored Swift/Kotlin oversized-file faults -pass. +pass. Activation parsing is also exact on both platforms: only `active` and an +optional distinct `previous` string hash are accepted, while the mirrored +shape/type/trailing-token corpus fails with +`mobile_web_activation_invalid`. The standalone renderer-based workspace, session, files, terminal, source-control, and review presentation is also removed with its Vite-only package path. A production-source boundary requires `src/mobile-web/` to remain diff --git a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md index 374e88ae806..056042a3c02 100644 --- a/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md +++ b/docs/reference/plans/2026-07-22-mobile-hybrid-webview-single-pr-migration.md @@ -2732,10 +2732,12 @@ JSON scalar types. The shared chunk envelope also caps base64 at the exact encoding, decoded-length mismatch, and extra fields. Swift and Kotlin enforce that encoded ceiling before invoking their native decoders and cap each raw manifest document at 256 KiB before JSON parsing. Native activation metadata -likewise requires string-typed active and previous hashes on both platforms; -hash-shaped JSON numbers fail with -`mobile_web_activation_invalid`. Manifest and package-RPC schemas now share one -exact asset-path predicate. A mirrored TypeScript, Swift, and Kotlin corpus +now accepts only an exact object containing string-typed `active` and optional +distinct `previous` hashes. A mirrored Swift/Kotlin corpus rejects missing, +null, Boolean, numeric, array, uppercase, duplicate-generation, unknown-field, +and trailing-token mutations with `mobile_web_activation_invalid`. Manifest +and package-RPC schemas now share one exact asset-path predicate. A mirrored +TypeScript, Swift, and Kotlin corpus rejects empty, absolute, traversal, repeated-separator, percent-encoded, query, fragment, backslash, non-ASCII, overlong, and trailing-newline paths while accepting the reviewed relative form. Shared application SHA-256, Git object @@ -2752,7 +2754,7 @@ canonical manifest reads at 256 KiB, activation records at 1 KiB, and asset reads at the manifest-declared length before hashing or parsing. Each reader consumes at most one overflow byte, and mirrored Swift/Kotlin faults preserve the stable generation and activation errors. Generated mutation and the -remaining CSP/cache corpus are still required. +remaining CSP/generation-directory cache corpus are still required. ## App Store Gate diff --git a/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md b/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md index c7e1c439185..e582ab13f13 100644 --- a/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md +++ b/docs/reference/plans/2026-07-27-mobile-hybrid-webview-remaining-work.md @@ -206,6 +206,11 @@ Post-runbook validation passes 569 mobile files / 3,378 tests with 2 expected skips. Mobile and mobile-web typechecks/lints, reliability, max-lines, formatting, diff hygiene, and the unchanged `b17ead7a…` package verification pass. +Native activation metadata now consumes one exact object on both platforms. +The mirrored two-valid/eleven-invalid corpus closes unknown fields, null and +non-string hashes, identical active/previous generations, and trailing tokens. +The iOS native fault executable, Android Debug unit/Release Kotlin gates, and +the same broader validation pass. The remaining security work below is release-app corpus testing, fuzzing, cross-scope races, privacy/authorization audit, and independent review. @@ -244,9 +249,11 @@ cross-scope races, privacy/authorization audit, and independent review. `NSNumber`/`CFBoolean` integer bridging. Chunk base64 is capped at 65,536 characters in the shared schema and both native stores before decode; its bounded request/chunk mutation corpus passes. Native activation metadata - rejects numeric active/previous hashes with the same stable error on both - platforms. Both native stores cap each raw manifest at 256 KiB before JSON - parsing. Android now requires the exact root document URL and rejects + accepts only exact `active` and optional distinct `previous` string + hashes. Its mirrored missing/null/Boolean/numeric/array/uppercase/ + duplicate/unknown/trailing-token corpus fails with the same stable error + on both platforms. Both native stores cap each raw manifest at 256 KiB + before JSON parsing. Android now requires the exact root document URL and rejects percent-encoded or query-bearing asset requests. One document-CSP contract now drives packaging/verification and exact native source parity. Manifest and package RPC reuse one exact path predicate, and the same diff --git a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebActivationMetadata.kt b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebActivationMetadata.kt new file mode 100644 index 00000000000..385a5541d2a --- /dev/null +++ b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebActivationMetadata.kt @@ -0,0 +1,40 @@ +package expo.modules.mobilewebshell + +import org.json.JSONObject +import org.json.JSONTokener + +internal data class MobileWebActivationRecord( + val active: String, + val previous: String? +) + +internal fun parseMobileWebActivationRecord(json: String): MobileWebActivationRecord { + val value = try { + val tokens = JSONTokener(json) + val candidate = tokens.nextValue() + require(candidate is JSONObject && tokens.nextClean() == '\u0000') + candidate + } catch (_: Exception) { + throw invalidActivation() + } + val keys = value.keys().asSequence().toSet() + require(keys == setOf("active") || keys == setOf("active", "previous")) { + "mobile_web_activation_invalid" + } + val active = value.opt("active") as? String ?: throw invalidActivation() + val previous = if ("previous" in keys) { + value.opt("previous") as? String ?: throw invalidActivation() + } else { + null + } + require( + isMobileWebSha256(active) && + (previous == null || isMobileWebSha256(previous) && previous != active) + ) { + "mobile_web_activation_invalid" + } + return MobileWebActivationRecord(active, previous) +} + +private fun invalidActivation(): IllegalArgumentException = + IllegalArgumentException("mobile_web_activation_invalid") diff --git a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt index 0aab4edbb2a..a92d9b9bb1f 100644 --- a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt +++ b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt @@ -458,23 +458,13 @@ internal class MobileWebPackageStore internal constructor( } private fun readActivation(hostRoot: File): Pair = try { - val value = parseJsonObject( + parseMobileWebActivationRecord( readMobileWebFile( File(hostRoot, "activation.json"), ACTIVATION_JSON_BYTE_LIMIT, "mobile_web_activation_invalid" ).toString(Charsets.UTF_8) - ) - require(value.keys().asSequence().toSet().let { it == setOf("active") || it == setOf("active", "previous") }) - val active = strictJsonString(value, "active") ?: "" - val previous = if (value.has("previous") && !value.isNull("previous")) { - strictJsonString(value, "previous") - ?: throw IllegalArgumentException("mobile_web_activation_invalid") - } else { - null - } - require(isMobileWebSha256(active) && (previous == null || isMobileWebSha256(previous))) - active to previous + ).let { it.active to it.previous } } catch (_: Exception) { throw IllegalArgumentException("mobile_web_activation_invalid") } diff --git a/mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebActivationMetadataTest.kt b/mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebActivationMetadataTest.kt new file mode 100644 index 00000000000..a924a27289c --- /dev/null +++ b/mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebActivationMetadataTest.kt @@ -0,0 +1,42 @@ +package expo.modules.mobilewebshell + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Test + +class MobileWebActivationMetadataTest { + @Test + fun acceptsOnlyExactActivationMetadata() { + val active = "a".repeat(64) + val previous = "b".repeat(64) + assertEquals( + MobileWebActivationRecord(active, null), + parseMobileWebActivationRecord("""{"active":"$active"}""") + ) + assertEquals( + MobileWebActivationRecord(active, previous), + parseMobileWebActivationRecord("""{"active":"$active","previous":"$previous"}""") + ) + + val numericHash = "1".repeat(64) + val invalid = listOf( + "[]", + "{}", + """{"active":null}""", + """{"active":true}""", + """{"active":$numericHash}""", + """{"active":"${active.uppercase()}"}""", + """{"active":"$active","previous":null}""", + """{"active":"$active","previous":true}""", + """{"active":"$active","previous":"$active"}""", + """{"active":"$active","unexpected":true}""", + """{"active":"$active"} trailing""" + ) + invalid.forEach { value -> + val error = assertThrows(IllegalArgumentException::class.java) { + parseMobileWebActivationRecord(value) + } + assertEquals("mobile_web_activation_invalid", error.message) + } + } +} diff --git a/mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebPackageStoreTest.kt b/mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebPackageStoreTest.kt index 2f7c888ee5c..925438f0dfd 100644 --- a/mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebPackageStoreTest.kt +++ b/mobile/packages/expo-mobile-web-shell/android/src/test/java/expo/modules/mobilewebshell/MobileWebPackageStoreTest.kt @@ -381,27 +381,6 @@ class MobileWebPackageStoreTest { assertFalse(currentDocument.exists()) } - @Test - fun rejectsNumericActivationFields() { - val root = temporary.newFolder() - val store = MobileWebPackageStore(root) - val hostRoot = File(root, sha256Hex("paired-host".toByteArray())) - require(hostRoot.mkdirs()) - val numericHash = "1".repeat(64) - val validHash = "a".repeat(64) - - listOf( - """{"active":$numericHash}""", - """{"active":"$validHash","previous":$numericHash}""" - ).forEach { activation -> - File(hostRoot, "activation.json").writeText(activation) - val error = assertThrows(IllegalArgumentException::class.java) { - store.openSession("paired-host", null, 1) - } - assertEquals("mobile_web_activation_invalid", error.message) - } - } - @Test fun rejectsLowStorageBeforeCreatingAStage() { val root = temporary.newFolder() diff --git a/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebActivationMetadataTests.swift b/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebActivationMetadataTests.swift new file mode 100644 index 00000000000..9290169f32e --- /dev/null +++ b/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebActivationMetadataTests.swift @@ -0,0 +1,69 @@ +import CryptoKit +import Foundation + +enum MobileWebActivationMetadataTests { + static func run(root: URL) throws { + let active = String(repeating: "a", count: 64) + let previous = String(repeating: "b", count: 64) + let valid = [ + "{\"active\":\"\(active)\"}", + "{\"active\":\"\(active)\",\"previous\":\"\(previous)\"}", + ] + precondition( + valid.allSatisfy { + parseMobileWebActivationRecord(Data($0.utf8)) != nil + } + ) + + let numericHash = String(repeating: "1", count: 64) + let invalid = [ + "[]", + "{}", + "{\"active\":null}", + "{\"active\":true}", + "{\"active\":\(numericHash)}", + "{\"active\":\"\(active.uppercased())\"}", + "{\"active\":\"\(active)\",\"previous\":null}", + "{\"active\":\"\(active)\",\"previous\":true}", + "{\"active\":\"\(active)\",\"previous\":\"\(active)\"}", + "{\"active\":\"\(active)\",\"unexpected\":true}", + "{\"active\":\"\(active)\"} trailing", + ] + precondition( + invalid.allSatisfy { + parseMobileWebActivationRecord(Data($0.utf8)) == nil + } + ) + + let store = MobileWebPackageStore(cacheRoot: root) + let hostRoot = + root + .appendingPathComponent(sha256Hex(Data("paired-host".utf8))) + try FileManager.default.createDirectory(at: hostRoot, withIntermediateDirectories: true) + for value in invalid { + try Data(value.utf8).write(to: hostRoot.appendingPathComponent("activation.json")) + precondition( + throwsCode("mobile_web_activation_invalid") { + _ = try store.openSession( + hostIdentity: "paired-host", + buildId: nil, + bridgeVersion: 1 + ) + } + ) + } + } + + private static func throwsCode(_ code: String, _ body: () throws -> Void) -> Bool { + do { + try body() + return false + } catch { + return error.localizedDescription == code + } + } + + private static func sha256Hex(_ data: Data) -> String { + SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined() + } +} diff --git a/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebPackageStoreTests.swift b/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebPackageStoreTests.swift index 679e4ab66e2..810bf39dc6c 100644 --- a/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebPackageStoreTests.swift +++ b/mobile/packages/expo-mobile-web-shell/ios-tests/MobileWebPackageStoreTests.swift @@ -25,7 +25,9 @@ enum MobileWebPackageStoreTests { try rejectsOversizedPersistedFiles(root: root.appendingPathComponent("persisted-limits")) try activatesAndRecoversPreviousGeneration(root: root.appendingPathComponent("rollback")) try fallsBackFromCorruptActiveGeneration(root: root.appendingPathComponent("corrupt-active")) - try rejectsNumericActivationFields(root: root.appendingPathComponent("activation-types")) + try MobileWebActivationMetadataTests.run( + root: root.appendingPathComponent("activation-types") + ) try rejectsLowStorage(root: root.appendingPathComponent("low-storage")) try evictsUnprotectedGeneration(root: root.appendingPathComponent("eviction")) try evictsAnotherHostForGlobalQuota(root: root.appendingPathComponent("global-eviction")) @@ -513,31 +515,6 @@ enum MobileWebPackageStoreTests { precondition(!FileManager.default.fileExists(atPath: currentDocument.path)) } - private static func rejectsNumericActivationFields(root: URL) throws { - let store = MobileWebPackageStore(cacheRoot: root) - let hostRoot = root.appendingPathComponent(sha256Hex(Data("paired-host".utf8))) - try FileManager.default.createDirectory(at: hostRoot, withIntermediateDirectories: true) - let numericHash = String(repeating: "1", count: 64) - let validHash = String(repeating: "a", count: 64) - let invalid = [ - #"{"active":\#(numericHash)}"#, - #"{"active":"\#(validHash)","previous":\#(numericHash)}"#, - ] - - for activation in invalid { - try Data(activation.utf8).write(to: hostRoot.appendingPathComponent("activation.json")) - precondition( - throwsCode("mobile_web_activation_invalid") { - _ = try store.openSession( - hostIdentity: "paired-host", - buildId: nil, - bridgeVersion: 1 - ) - } - ) - } - } - private static func rejectsLowStorage(root: URL) throws { let store = MobileWebPackageStore( cacheRoot: root, diff --git a/mobile/packages/expo-mobile-web-shell/ios/MobileWebActivationMetadata.swift b/mobile/packages/expo-mobile-web-shell/ios/MobileWebActivationMetadata.swift new file mode 100644 index 00000000000..687d7f30eef --- /dev/null +++ b/mobile/packages/expo-mobile-web-shell/ios/MobileWebActivationMetadata.swift @@ -0,0 +1,31 @@ +import Foundation + +struct MobileWebActivationRecord: Codable, Equatable { + let active: String + let previous: String? +} + +func parseMobileWebActivationRecord(_ data: Data) -> MobileWebActivationRecord? { + guard + let value = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + Set(value.keys) == Set(["active"]) || Set(value.keys) == Set(["active", "previous"]), + let active = value["active"] as? String, + isMobileWebSha256(active) + else { + return nil + } + let previous: String? + if value.keys.contains("previous") { + guard + let candidate = value["previous"] as? String, + isMobileWebSha256(candidate), + candidate != active + else { + return nil + } + previous = candidate + } else { + previous = nil + } + return MobileWebActivationRecord(active: active, previous: previous) +} diff --git a/mobile/packages/expo-mobile-web-shell/ios/MobileWebPackageStore.swift b/mobile/packages/expo-mobile-web-shell/ios/MobileWebPackageStore.swift index b20f90eac73..e9654cc229a 100644 --- a/mobile/packages/expo-mobile-web-shell/ios/MobileWebPackageStore.swift +++ b/mobile/packages/expo-mobile-web-shell/ios/MobileWebPackageStore.swift @@ -41,11 +41,6 @@ struct MobileWebAssetResponse { let isDocument: Bool } -private struct MobileWebActivationRecord: Codable { - let active: String - let previous: String? -} - private final class MobileWebSessionRecord { let hostKey: String let buildId: String @@ -648,11 +643,7 @@ final class MobileWebPackageStore { byteLimit: activationJsonByteLimit, overflowCode: "mobile_web_activation_invalid" ) - let activation = try JSONDecoder().decode(MobileWebActivationRecord.self, from: data) - guard - isMobileWebSha256(activation.active), - activation.previous == nil || isMobileWebSha256(activation.previous!) - else { + guard let activation = parseMobileWebActivationRecord(data) else { throw MobileWebStoreError("mobile_web_activation_invalid") } return activation diff --git a/mobile/scripts/run-ios-mobile-web-store-tests.mjs b/mobile/scripts/run-ios-mobile-web-store-tests.mjs index 211fa263509..8ab3e2f8988 100644 --- a/mobile/scripts/run-ios-mobile-web-store-tests.mjs +++ b/mobile/scripts/run-ios-mobile-web-store-tests.mjs @@ -19,7 +19,12 @@ try { 'swiftc', '-DMOBILE_WEB_PACKAGE_STORE_TESTING', join(mobileRoot, 'packages/expo-mobile-web-shell/ios/MobileWebCacheStoragePolicy.swift'), + join(mobileRoot, 'packages/expo-mobile-web-shell/ios/MobileWebActivationMetadata.swift'), join(mobileRoot, 'packages/expo-mobile-web-shell/ios/MobileWebPackageStore.swift'), + join( + mobileRoot, + 'packages/expo-mobile-web-shell/ios-tests/MobileWebActivationMetadataTests.swift' + ), join( mobileRoot, 'packages/expo-mobile-web-shell/ios-tests/MobileWebPackageStoreProcessInterruptionTests.swift'