@rs-native-kit/version-check 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +256 -0
- package/VersionCheck.podspec +20 -0
- package/android/build.gradle +56 -0
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/java/com/rsnativekit/versioncheck/SecureStorageCipher.kt +57 -0
- package/android/src/main/java/com/rsnativekit/versioncheck/VersionCheckHeadlessTaskService.kt +31 -0
- package/android/src/main/java/com/rsnativekit/versioncheck/VersionCheckModule.kt +215 -0
- package/android/src/main/java/com/rsnativekit/versioncheck/VersionCheckPackage.kt +31 -0
- package/android/src/main/java/com/rsnativekit/versioncheck/VersionCheckWorker.kt +29 -0
- package/ios/PlatformBridge/Ed25519Verifier.swift +19 -0
- package/ios/VersionCheck.h +5 -0
- package/ios/VersionCheck.mm +278 -0
- package/lib/module/data/cache/MemoryCacheStore.js +30 -0
- package/lib/module/data/cache/MemoryCacheStore.js.map +1 -0
- package/lib/module/data/cache/PersistentCacheStore.js +56 -0
- package/lib/module/data/cache/PersistentCacheStore.js.map +1 -0
- package/lib/module/data/config/ConfigCache.js +29 -0
- package/lib/module/data/config/ConfigCache.js.map +1 -0
- package/lib/module/data/config/ConfigDocumentValidator.js +185 -0
- package/lib/module/data/config/ConfigDocumentValidator.js.map +1 -0
- package/lib/module/data/config/ConfigLoader.js +82 -0
- package/lib/module/data/config/ConfigLoader.js.map +1 -0
- package/lib/module/data/config/ConfigProvider.js +58 -0
- package/lib/module/data/config/ConfigProvider.js.map +1 -0
- package/lib/module/data/config/ConfigResolutionPipeline.js +268 -0
- package/lib/module/data/config/ConfigResolutionPipeline.js.map +1 -0
- package/lib/module/data/config/ConfigValidator.js +69 -0
- package/lib/module/data/config/ConfigValidator.js.map +1 -0
- package/lib/module/data/config/SignatureVerifier.js +80 -0
- package/lib/module/data/config/SignatureVerifier.js.map +1 -0
- package/lib/module/data/config/canonicalize.js +88 -0
- package/lib/module/data/config/canonicalize.js.map +1 -0
- package/lib/module/data/config/sources/EnvironmentOverrideSource.js +20 -0
- package/lib/module/data/config/sources/EnvironmentOverrideSource.js.map +1 -0
- package/lib/module/data/config/sources/LocalConfigSource.js +22 -0
- package/lib/module/data/config/sources/LocalConfigSource.js.map +1 -0
- package/lib/module/data/config/sources/RemoteConfigSource.js +38 -0
- package/lib/module/data/config/sources/RemoteConfigSource.js.map +1 -0
- package/lib/module/data/providers/amazon/AmazonAppstoreProvider.js +13 -0
- package/lib/module/data/providers/amazon/AmazonAppstoreProvider.js.map +1 -0
- package/lib/module/data/providers/apple/AppleStoreProvider.js +67 -0
- package/lib/module/data/providers/apple/AppleStoreProvider.js.map +1 -0
- package/lib/module/data/providers/custom-api/CustomApiProvider.js +51 -0
- package/lib/module/data/providers/custom-api/CustomApiProvider.js.map +1 -0
- package/lib/module/data/providers/firebase-remote-config/FirebaseRemoteConfigProvider.js +13 -0
- package/lib/module/data/providers/firebase-remote-config/FirebaseRemoteConfigProvider.js.map +1 -0
- package/lib/module/data/providers/google-play/GooglePlayProvider.js +62 -0
- package/lib/module/data/providers/google-play/GooglePlayProvider.js.map +1 -0
- package/lib/module/data/providers/huawei/HuaweiAppGalleryProvider.js +18 -0
- package/lib/module/data/providers/huawei/HuaweiAppGalleryProvider.js.map +1 -0
- package/lib/module/data/repositories/VersionRepositoryImpl.js +69 -0
- package/lib/module/data/repositories/VersionRepositoryImpl.js.map +1 -0
- package/lib/module/di/container.js +80 -0
- package/lib/module/di/container.js.map +1 -0
- package/lib/module/domain/IVersionManagerCore.js +4 -0
- package/lib/module/domain/IVersionManagerCore.js.map +1 -0
- package/lib/module/domain/VersionManagerCore.js +276 -0
- package/lib/module/domain/VersionManagerCore.js.map +1 -0
- package/lib/module/domain/engines/decision/DecisionEngine.js +40 -0
- package/lib/module/domain/engines/decision/DecisionEngine.js.map +1 -0
- package/lib/module/domain/engines/policy/IUpdatePolicyEngine.js +4 -0
- package/lib/module/domain/engines/policy/IUpdatePolicyEngine.js.map +1 -0
- package/lib/module/domain/engines/policy/PolicyEngine.js +42 -0
- package/lib/module/domain/engines/policy/PolicyEngine.js.map +1 -0
- package/lib/module/domain/engines/semver/IVersionComparator.js +4 -0
- package/lib/module/domain/engines/semver/IVersionComparator.js.map +1 -0
- package/lib/module/domain/engines/semver/SemVerEngine.js +176 -0
- package/lib/module/domain/engines/semver/SemVerEngine.js.map +1 -0
- package/lib/module/domain/errors/VersionManagerException.js +104 -0
- package/lib/module/domain/errors/VersionManagerException.js.map +1 -0
- package/lib/module/domain/models/ActionPlan.js +9 -0
- package/lib/module/domain/models/ActionPlan.js.map +1 -0
- package/lib/module/domain/models/ConfigDocument.js +4 -0
- package/lib/module/domain/models/ConfigDocument.js.map +1 -0
- package/lib/module/domain/models/Events.js +4 -0
- package/lib/module/domain/models/Events.js.map +1 -0
- package/lib/module/domain/models/LifecycleState.js +29 -0
- package/lib/module/domain/models/LifecycleState.js.map +1 -0
- package/lib/module/domain/models/ParsedVersion.js +2 -0
- package/lib/module/domain/models/ParsedVersion.js.map +1 -0
- package/lib/module/domain/models/PlatformId.js +2 -0
- package/lib/module/domain/models/PlatformId.js.map +1 -0
- package/lib/module/domain/models/PublicApiOptions.js +2 -0
- package/lib/module/domain/models/PublicApiOptions.js.map +1 -0
- package/lib/module/domain/models/RemoteVersionInfo.js +4 -0
- package/lib/module/domain/models/RemoteVersionInfo.js.map +1 -0
- package/lib/module/domain/models/StoreLookup.js +2 -0
- package/lib/module/domain/models/StoreLookup.js.map +1 -0
- package/lib/module/domain/models/Unsubscribe.js +2 -0
- package/lib/module/domain/models/Unsubscribe.js.map +1 -0
- package/lib/module/domain/models/UpdateInfo.js +4 -0
- package/lib/module/domain/models/UpdateInfo.js.map +1 -0
- package/lib/module/domain/models/VersionManagerOptions.js +4 -0
- package/lib/module/domain/models/VersionManagerOptions.js.map +1 -0
- package/lib/module/domain/ports/ICacheStore.js +2 -0
- package/lib/module/domain/ports/ICacheStore.js.map +1 -0
- package/lib/module/domain/ports/IClock.js +2 -0
- package/lib/module/domain/ports/IClock.js.map +1 -0
- package/lib/module/domain/ports/IConfigProvider.js +4 -0
- package/lib/module/domain/ports/IConfigProvider.js.map +1 -0
- package/lib/module/domain/ports/IConfigSources.js +4 -0
- package/lib/module/domain/ports/IConfigSources.js.map +1 -0
- package/lib/module/domain/ports/IPlatformBridge.js +4 -0
- package/lib/module/domain/ports/IPlatformBridge.js.map +1 -0
- package/lib/module/domain/ports/IStoreProvider.js +4 -0
- package/lib/module/domain/ports/IStoreProvider.js.map +1 -0
- package/lib/module/domain/ports/IVersionRepository.js +4 -0
- package/lib/module/domain/ports/IVersionRepository.js.map +1 -0
- package/lib/module/domain/statemachine/LifecycleStateMachine.js +55 -0
- package/lib/module/domain/statemachine/LifecycleStateMachine.js.map +1 -0
- package/lib/module/domain/usecases/CheckForUpdatesUseCase.js +31 -0
- package/lib/module/domain/usecases/CheckForUpdatesUseCase.js.map +1 -0
- package/lib/module/domain/usecases/ForceTriggerUpdateUseCase.js +20 -0
- package/lib/module/domain/usecases/ForceTriggerUpdateUseCase.js.map +1 -0
- package/lib/module/domain/usecases/ResetIgnoredVersionsUseCase.js +24 -0
- package/lib/module/domain/usecases/ResetIgnoredVersionsUseCase.js.map +1 -0
- package/lib/module/index.js +10 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/platform/contracts/IPlatformBridge.js +2 -0
- package/lib/module/platform/contracts/IPlatformBridge.js.map +1 -0
- package/lib/module/platform/createPlatformBridge.js +17 -0
- package/lib/module/platform/createPlatformBridge.js.map +1 -0
- package/lib/module/platform/createPlatformBridge.native.js +9 -0
- package/lib/module/platform/createPlatformBridge.native.js.map +1 -0
- package/lib/module/platform/native/NativePlatformBridge.js +143 -0
- package/lib/module/platform/native/NativePlatformBridge.js.map +1 -0
- package/lib/module/platform/native/NativeVersionCheck.js +33 -0
- package/lib/module/platform/native/NativeVersionCheck.js.map +1 -0
- package/lib/module/platform/web/WebPlatformBridge.js +228 -0
- package/lib/module/platform/web/WebPlatformBridge.js.map +1 -0
- package/lib/module/presentation/components/ForceUpdateScreen.js +68 -0
- package/lib/module/presentation/components/ForceUpdateScreen.js.map +1 -0
- package/lib/module/presentation/components/OptionalUpdateBanner.js +69 -0
- package/lib/module/presentation/components/OptionalUpdateBanner.js.map +1 -0
- package/lib/module/presentation/components/SoftUpdateDialog.js +101 -0
- package/lib/module/presentation/components/SoftUpdateDialog.js.map +1 -0
- package/lib/module/presentation/hooks/VersionManagerProvider.js +18 -0
- package/lib/module/presentation/hooks/VersionManagerProvider.js.map +1 -0
- package/lib/module/presentation/hooks/context.js +5 -0
- package/lib/module/presentation/hooks/context.js.map +1 -0
- package/lib/module/presentation/hooks/useUpdateState.js +26 -0
- package/lib/module/presentation/hooks/useUpdateState.js.map +1 -0
- package/lib/module/presentation/hooks/useVersionManager.js +33 -0
- package/lib/module/presentation/hooks/useVersionManager.js.map +1 -0
- package/lib/module/presentation/state/VersionManagerStore.js +47 -0
- package/lib/module/presentation/state/VersionManagerStore.js.map +1 -0
- package/lib/module/publicApi.js +114 -0
- package/lib/module/publicApi.js.map +1 -0
- package/lib/module/shared/eventbus/EventBus.js +34 -0
- package/lib/module/shared/eventbus/EventBus.js.map +1 -0
- package/lib/module/shared/logging/Logger.js +56 -0
- package/lib/module/shared/logging/Logger.js.map +1 -0
- package/lib/module/ui.js +13 -0
- package/lib/module/ui.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/data/cache/MemoryCacheStore.d.ts +12 -0
- package/lib/typescript/src/data/cache/MemoryCacheStore.d.ts.map +1 -0
- package/lib/typescript/src/data/cache/PersistentCacheStore.d.ts +14 -0
- package/lib/typescript/src/data/cache/PersistentCacheStore.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigCache.d.ts +15 -0
- package/lib/typescript/src/data/config/ConfigCache.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigDocumentValidator.d.ts +20 -0
- package/lib/typescript/src/data/config/ConfigDocumentValidator.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigLoader.d.ts +28 -0
- package/lib/typescript/src/data/config/ConfigLoader.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigProvider.d.ts +30 -0
- package/lib/typescript/src/data/config/ConfigProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigResolutionPipeline.d.ts +49 -0
- package/lib/typescript/src/data/config/ConfigResolutionPipeline.d.ts.map +1 -0
- package/lib/typescript/src/data/config/ConfigValidator.d.ts +10 -0
- package/lib/typescript/src/data/config/ConfigValidator.d.ts.map +1 -0
- package/lib/typescript/src/data/config/SignatureVerifier.d.ts +16 -0
- package/lib/typescript/src/data/config/SignatureVerifier.d.ts.map +1 -0
- package/lib/typescript/src/data/config/canonicalize.d.ts +13 -0
- package/lib/typescript/src/data/config/canonicalize.d.ts.map +1 -0
- package/lib/typescript/src/data/config/sources/EnvironmentOverrideSource.d.ts +17 -0
- package/lib/typescript/src/data/config/sources/EnvironmentOverrideSource.d.ts.map +1 -0
- package/lib/typescript/src/data/config/sources/LocalConfigSource.d.ts +19 -0
- package/lib/typescript/src/data/config/sources/LocalConfigSource.d.ts.map +1 -0
- package/lib/typescript/src/data/config/sources/RemoteConfigSource.d.ts +10 -0
- package/lib/typescript/src/data/config/sources/RemoteConfigSource.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/amazon/AmazonAppstoreProvider.d.ts +9 -0
- package/lib/typescript/src/data/providers/amazon/AmazonAppstoreProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/apple/AppleStoreProvider.d.ts +19 -0
- package/lib/typescript/src/data/providers/apple/AppleStoreProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/custom-api/CustomApiProvider.d.ts +14 -0
- package/lib/typescript/src/data/providers/custom-api/CustomApiProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/firebase-remote-config/FirebaseRemoteConfigProvider.d.ts +9 -0
- package/lib/typescript/src/data/providers/firebase-remote-config/FirebaseRemoteConfigProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/google-play/GooglePlayProvider.d.ts +22 -0
- package/lib/typescript/src/data/providers/google-play/GooglePlayProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/providers/huawei/HuaweiAppGalleryProvider.d.ts +14 -0
- package/lib/typescript/src/data/providers/huawei/HuaweiAppGalleryProvider.d.ts.map +1 -0
- package/lib/typescript/src/data/repositories/VersionRepositoryImpl.d.ts +23 -0
- package/lib/typescript/src/data/repositories/VersionRepositoryImpl.d.ts.map +1 -0
- package/lib/typescript/src/di/container.d.ts +12 -0
- package/lib/typescript/src/di/container.d.ts.map +1 -0
- package/lib/typescript/src/domain/IVersionManagerCore.d.ts +30 -0
- package/lib/typescript/src/domain/IVersionManagerCore.d.ts.map +1 -0
- package/lib/typescript/src/domain/VersionManagerCore.d.ts +94 -0
- package/lib/typescript/src/domain/VersionManagerCore.d.ts.map +1 -0
- package/lib/typescript/src/domain/engines/decision/DecisionEngine.d.ts +16 -0
- package/lib/typescript/src/domain/engines/decision/DecisionEngine.d.ts.map +1 -0
- package/lib/typescript/src/domain/engines/policy/IUpdatePolicyEngine.d.ts +17 -0
- package/lib/typescript/src/domain/engines/policy/IUpdatePolicyEngine.d.ts.map +1 -0
- package/lib/typescript/src/domain/engines/policy/PolicyEngine.d.ts +16 -0
- package/lib/typescript/src/domain/engines/policy/PolicyEngine.d.ts.map +1 -0
- package/lib/typescript/src/domain/engines/semver/IVersionComparator.d.ts +7 -0
- package/lib/typescript/src/domain/engines/semver/IVersionComparator.d.ts.map +1 -0
- package/lib/typescript/src/domain/engines/semver/SemVerEngine.d.ts +11 -0
- package/lib/typescript/src/domain/engines/semver/SemVerEngine.d.ts.map +1 -0
- package/lib/typescript/src/domain/errors/VersionManagerException.d.ts +97 -0
- package/lib/typescript/src/domain/errors/VersionManagerException.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/ActionPlan.d.ts +15 -0
- package/lib/typescript/src/domain/models/ActionPlan.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/ConfigDocument.d.ts +86 -0
- package/lib/typescript/src/domain/models/ConfigDocument.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/Events.d.ts +39 -0
- package/lib/typescript/src/domain/models/Events.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/LifecycleState.d.ts +18 -0
- package/lib/typescript/src/domain/models/LifecycleState.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/ParsedVersion.d.ts +9 -0
- package/lib/typescript/src/domain/models/ParsedVersion.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/PlatformId.d.ts +3 -0
- package/lib/typescript/src/domain/models/PlatformId.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/PublicApiOptions.d.ts +13 -0
- package/lib/typescript/src/domain/models/PublicApiOptions.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/RemoteVersionInfo.d.ts +21 -0
- package/lib/typescript/src/domain/models/RemoteVersionInfo.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/StoreLookup.d.ts +12 -0
- package/lib/typescript/src/domain/models/StoreLookup.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/Unsubscribe.d.ts +2 -0
- package/lib/typescript/src/domain/models/Unsubscribe.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/UpdateInfo.d.ts +11 -0
- package/lib/typescript/src/domain/models/UpdateInfo.d.ts.map +1 -0
- package/lib/typescript/src/domain/models/VersionManagerOptions.d.ts +149 -0
- package/lib/typescript/src/domain/models/VersionManagerOptions.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/ICacheStore.d.ts +6 -0
- package/lib/typescript/src/domain/ports/ICacheStore.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IClock.d.ts +4 -0
- package/lib/typescript/src/domain/ports/IClock.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IConfigProvider.d.ts +11 -0
- package/lib/typescript/src/domain/ports/IConfigProvider.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IConfigSources.d.ts +25 -0
- package/lib/typescript/src/domain/ports/IConfigSources.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IPlatformBridge.d.ts +77 -0
- package/lib/typescript/src/domain/ports/IPlatformBridge.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IStoreProvider.d.ts +7 -0
- package/lib/typescript/src/domain/ports/IStoreProvider.d.ts.map +1 -0
- package/lib/typescript/src/domain/ports/IVersionRepository.d.ts +8 -0
- package/lib/typescript/src/domain/ports/IVersionRepository.d.ts.map +1 -0
- package/lib/typescript/src/domain/statemachine/LifecycleStateMachine.d.ts +25 -0
- package/lib/typescript/src/domain/statemachine/LifecycleStateMachine.d.ts.map +1 -0
- package/lib/typescript/src/domain/usecases/CheckForUpdatesUseCase.d.ts +30 -0
- package/lib/typescript/src/domain/usecases/CheckForUpdatesUseCase.d.ts.map +1 -0
- package/lib/typescript/src/domain/usecases/ForceTriggerUpdateUseCase.d.ts +17 -0
- package/lib/typescript/src/domain/usecases/ForceTriggerUpdateUseCase.d.ts.map +1 -0
- package/lib/typescript/src/domain/usecases/ResetIgnoredVersionsUseCase.d.ts +7 -0
- package/lib/typescript/src/domain/usecases/ResetIgnoredVersionsUseCase.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +17 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/platform/contracts/IPlatformBridge.d.ts +2 -0
- package/lib/typescript/src/platform/contracts/IPlatformBridge.d.ts.map +1 -0
- package/lib/typescript/src/platform/createPlatformBridge.d.ts +12 -0
- package/lib/typescript/src/platform/createPlatformBridge.d.ts.map +1 -0
- package/lib/typescript/src/platform/createPlatformBridge.native.d.ts +4 -0
- package/lib/typescript/src/platform/createPlatformBridge.native.d.ts.map +1 -0
- package/lib/typescript/src/platform/native/NativePlatformBridge.d.ts +17 -0
- package/lib/typescript/src/platform/native/NativePlatformBridge.d.ts.map +1 -0
- package/lib/typescript/src/platform/native/NativeVersionCheck.d.ts +45 -0
- package/lib/typescript/src/platform/native/NativeVersionCheck.d.ts.map +1 -0
- package/lib/typescript/src/platform/web/WebPlatformBridge.d.ts +17 -0
- package/lib/typescript/src/platform/web/WebPlatformBridge.d.ts.map +1 -0
- package/lib/typescript/src/presentation/components/ForceUpdateScreen.d.ts +12 -0
- package/lib/typescript/src/presentation/components/ForceUpdateScreen.d.ts.map +1 -0
- package/lib/typescript/src/presentation/components/OptionalUpdateBanner.d.ts +12 -0
- package/lib/typescript/src/presentation/components/OptionalUpdateBanner.d.ts.map +1 -0
- package/lib/typescript/src/presentation/components/SoftUpdateDialog.d.ts +14 -0
- package/lib/typescript/src/presentation/components/SoftUpdateDialog.d.ts.map +1 -0
- package/lib/typescript/src/presentation/hooks/VersionManagerProvider.d.ts +9 -0
- package/lib/typescript/src/presentation/hooks/VersionManagerProvider.d.ts.map +1 -0
- package/lib/typescript/src/presentation/hooks/context.d.ts +3 -0
- package/lib/typescript/src/presentation/hooks/context.d.ts.map +1 -0
- package/lib/typescript/src/presentation/hooks/useUpdateState.d.ts +17 -0
- package/lib/typescript/src/presentation/hooks/useUpdateState.d.ts.map +1 -0
- package/lib/typescript/src/presentation/hooks/useVersionManager.d.ts +22 -0
- package/lib/typescript/src/presentation/hooks/useVersionManager.d.ts.map +1 -0
- package/lib/typescript/src/presentation/state/VersionManagerStore.d.ts +28 -0
- package/lib/typescript/src/presentation/state/VersionManagerStore.d.ts.map +1 -0
- package/lib/typescript/src/publicApi.d.ts +24 -0
- package/lib/typescript/src/publicApi.d.ts.map +1 -0
- package/lib/typescript/src/shared/eventbus/EventBus.d.ts +21 -0
- package/lib/typescript/src/shared/eventbus/EventBus.d.ts.map +1 -0
- package/lib/typescript/src/shared/logging/Logger.d.ts +17 -0
- package/lib/typescript/src/shared/logging/Logger.d.ts.map +1 -0
- package/lib/typescript/src/ui.d.ts +15 -0
- package/lib/typescript/src/ui.d.ts.map +1 -0
- package/package.json +192 -0
- package/src/data/cache/MemoryCacheStore.ts +36 -0
- package/src/data/cache/PersistentCacheStore.ts +63 -0
- package/src/data/config/ConfigCache.ts +30 -0
- package/src/data/config/ConfigDocumentValidator.ts +309 -0
- package/src/data/config/ConfigLoader.ts +105 -0
- package/src/data/config/ConfigProvider.ts +84 -0
- package/src/data/config/ConfigResolutionPipeline.ts +424 -0
- package/src/data/config/ConfigValidator.ts +79 -0
- package/src/data/config/SignatureVerifier.ts +90 -0
- package/src/data/config/canonicalize.ts +111 -0
- package/src/data/config/sources/EnvironmentOverrideSource.ts +22 -0
- package/src/data/config/sources/LocalConfigSource.ts +24 -0
- package/src/data/config/sources/RemoteConfigSource.ts +41 -0
- package/src/data/providers/amazon/AmazonAppstoreProvider.ts +16 -0
- package/src/data/providers/apple/AppleStoreProvider.ts +94 -0
- package/src/data/providers/custom-api/CustomApiProvider.ts +75 -0
- package/src/data/providers/firebase-remote-config/FirebaseRemoteConfigProvider.ts +16 -0
- package/src/data/providers/google-play/GooglePlayProvider.ts +75 -0
- package/src/data/providers/huawei/HuaweiAppGalleryProvider.ts +21 -0
- package/src/data/repositories/VersionRepositoryImpl.ts +90 -0
- package/src/di/container.ts +128 -0
- package/src/domain/IVersionManagerCore.ts +54 -0
- package/src/domain/VersionManagerCore.ts +425 -0
- package/src/domain/engines/decision/DecisionEngine.ts +47 -0
- package/src/domain/engines/policy/IUpdatePolicyEngine.ts +18 -0
- package/src/domain/engines/policy/PolicyEngine.ts +60 -0
- package/src/domain/engines/semver/IVersionComparator.ts +7 -0
- package/src/domain/engines/semver/SemVerEngine.ts +200 -0
- package/src/domain/errors/VersionManagerException.ts +166 -0
- package/src/domain/models/ActionPlan.ts +17 -0
- package/src/domain/models/ConfigDocument.ts +97 -0
- package/src/domain/models/Events.ts +48 -0
- package/src/domain/models/LifecycleState.ts +50 -0
- package/src/domain/models/ParsedVersion.ts +8 -0
- package/src/domain/models/PlatformId.ts +9 -0
- package/src/domain/models/PublicApiOptions.ts +14 -0
- package/src/domain/models/RemoteVersionInfo.ts +23 -0
- package/src/domain/models/StoreLookup.ts +12 -0
- package/src/domain/models/Unsubscribe.ts +1 -0
- package/src/domain/models/UpdateInfo.ts +11 -0
- package/src/domain/models/VersionManagerOptions.ts +150 -0
- package/src/domain/ports/ICacheStore.ts +5 -0
- package/src/domain/ports/IClock.ts +3 -0
- package/src/domain/ports/IConfigProvider.ts +18 -0
- package/src/domain/ports/IConfigSources.ts +44 -0
- package/src/domain/ports/IPlatformBridge.ts +97 -0
- package/src/domain/ports/IStoreProvider.ts +12 -0
- package/src/domain/ports/IVersionRepository.ts +14 -0
- package/src/domain/statemachine/LifecycleStateMachine.ts +76 -0
- package/src/domain/usecases/CheckForUpdatesUseCase.ts +63 -0
- package/src/domain/usecases/ForceTriggerUpdateUseCase.ts +35 -0
- package/src/domain/usecases/ResetIgnoredVersionsUseCase.ts +26 -0
- package/src/index.tsx +84 -0
- package/src/platform/contracts/IPlatformBridge.ts +14 -0
- package/src/platform/createPlatformBridge.native.ts +7 -0
- package/src/platform/createPlatformBridge.ts +15 -0
- package/src/platform/native/NativePlatformBridge.ts +179 -0
- package/src/platform/native/NativeVersionCheck.ts +64 -0
- package/src/platform/web/WebPlatformBridge.ts +347 -0
- package/src/presentation/components/ForceUpdateScreen.tsx +64 -0
- package/src/presentation/components/OptionalUpdateBanner.tsx +53 -0
- package/src/presentation/components/SoftUpdateDialog.tsx +84 -0
- package/src/presentation/hooks/VersionManagerProvider.tsx +23 -0
- package/src/presentation/hooks/context.ts +6 -0
- package/src/presentation/hooks/useUpdateState.ts +50 -0
- package/src/presentation/hooks/useVersionManager.ts +68 -0
- package/src/presentation/state/VersionManagerStore.ts +72 -0
- package/src/publicApi.ts +126 -0
- package/src/shared/eventbus/EventBus.ts +56 -0
- package/src/shared/logging/Logger.ts +70 -0
- package/src/ui.tsx +19 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { AppState, Platform } from 'react-native';
|
|
2
|
+
import {
|
|
3
|
+
OfflineException,
|
|
4
|
+
RequestTimeoutException,
|
|
5
|
+
} from '../../domain/errors/VersionManagerException';
|
|
6
|
+
import type { PlatformId } from '../../domain/models/PlatformId';
|
|
7
|
+
import type {
|
|
8
|
+
BackgroundTaskDescriptor,
|
|
9
|
+
HttpRequest,
|
|
10
|
+
HttpResponse,
|
|
11
|
+
IAppInfoProvider,
|
|
12
|
+
IAppLifecycleObserver,
|
|
13
|
+
IBackgroundScheduler,
|
|
14
|
+
ICryptoProvider,
|
|
15
|
+
IDeviceInfoProvider,
|
|
16
|
+
IHttpClient,
|
|
17
|
+
IKeyValueStorage,
|
|
18
|
+
IPlatformBridge,
|
|
19
|
+
ISecureStorage,
|
|
20
|
+
Unsubscribe,
|
|
21
|
+
} from '../../domain/ports/IPlatformBridge';
|
|
22
|
+
import NativeVersionCheck from './NativeVersionCheck';
|
|
23
|
+
|
|
24
|
+
class RnHttpClient implements IHttpClient {
|
|
25
|
+
async request(req: HttpRequest): Promise<HttpResponse> {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timeout = setTimeout(() => controller.abort(), req.timeoutMs);
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(req.url, {
|
|
30
|
+
method: req.method,
|
|
31
|
+
headers: req.headers,
|
|
32
|
+
body: req.body,
|
|
33
|
+
signal: controller.signal,
|
|
34
|
+
});
|
|
35
|
+
const headers: Record<string, string> = {};
|
|
36
|
+
response.headers.forEach((value, key) => {
|
|
37
|
+
headers[key] = value;
|
|
38
|
+
});
|
|
39
|
+
const body = await response.text();
|
|
40
|
+
return { status: response.status, headers, body };
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
43
|
+
throw new RequestTimeoutException({
|
|
44
|
+
message: `Request to "${req.url}" timed out after ${req.timeoutMs}ms`,
|
|
45
|
+
cause: error,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
throw new OfflineException({
|
|
49
|
+
message: `Request to "${req.url}" failed`,
|
|
50
|
+
cause: error,
|
|
51
|
+
});
|
|
52
|
+
} finally {
|
|
53
|
+
clearTimeout(timeout);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class RnKeyValueStorage implements IKeyValueStorage {
|
|
59
|
+
async get(key: string): Promise<string | null> {
|
|
60
|
+
return NativeVersionCheck().storageGet(key);
|
|
61
|
+
}
|
|
62
|
+
async set(key: string, value: string): Promise<void> {
|
|
63
|
+
await NativeVersionCheck().storageSet(key, value);
|
|
64
|
+
}
|
|
65
|
+
async remove(key: string): Promise<void> {
|
|
66
|
+
await NativeVersionCheck().storageRemove(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class RnSecureStorage implements ISecureStorage {
|
|
71
|
+
async getItem(key: string): Promise<string | null> {
|
|
72
|
+
return NativeVersionCheck().secureStorageGet(key);
|
|
73
|
+
}
|
|
74
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
75
|
+
await NativeVersionCheck().secureStorageSet(key, value);
|
|
76
|
+
}
|
|
77
|
+
async removeItem(key: string): Promise<void> {
|
|
78
|
+
await NativeVersionCheck().secureStorageRemove(key);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class RnAppInfoProvider implements IAppInfoProvider {
|
|
83
|
+
async getCurrentVersion(): Promise<string> {
|
|
84
|
+
return NativeVersionCheck().getCurrentAppVersion();
|
|
85
|
+
}
|
|
86
|
+
async getBuildNumber(): Promise<string> {
|
|
87
|
+
return NativeVersionCheck().getBuildNumber();
|
|
88
|
+
}
|
|
89
|
+
async getBundleId(): Promise<string> {
|
|
90
|
+
return NativeVersionCheck().getBundleId();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class RnDeviceInfoProvider implements IDeviceInfoProvider {
|
|
95
|
+
async getOsVersion(): Promise<string> {
|
|
96
|
+
return NativeVersionCheck().getOsVersion();
|
|
97
|
+
}
|
|
98
|
+
async getDeviceModel(): Promise<string> {
|
|
99
|
+
return NativeVersionCheck().getDeviceModel();
|
|
100
|
+
}
|
|
101
|
+
async getLocale(): Promise<string> {
|
|
102
|
+
return NativeVersionCheck().getLocale();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* True OS-scheduled background execution (BGTaskScheduler/WorkManager, Prompt 26) is
|
|
108
|
+
* not implemented in this pass. schedule()/cancel() log a warning rather than silently
|
|
109
|
+
* pretending to work — see the equivalent, identically-scoped note on WebBackgroundScheduler.
|
|
110
|
+
*/
|
|
111
|
+
class RnBackgroundScheduler implements IBackgroundScheduler {
|
|
112
|
+
async schedule(task: BackgroundTaskDescriptor): Promise<void> {
|
|
113
|
+
console.warn(
|
|
114
|
+
`[VersionManager] Background scheduling ("${task.taskId}") is not implemented on ${Platform.OS} yet (Prompt 26).`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
async cancel(_taskId: string): Promise<void> {
|
|
118
|
+
// no-op — nothing was ever scheduled, see schedule() above.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class RnAppLifecycleObserver implements IAppLifecycleObserver {
|
|
123
|
+
onForeground(cb: () => void): Unsubscribe {
|
|
124
|
+
const sub = AppState.addEventListener('change', (state) => {
|
|
125
|
+
if (state === 'active') cb();
|
|
126
|
+
});
|
|
127
|
+
return () => sub.remove();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
onBackground(cb: () => void): Unsubscribe {
|
|
131
|
+
const sub = AppState.addEventListener('change', (state) => {
|
|
132
|
+
if (state === 'background') cb();
|
|
133
|
+
});
|
|
134
|
+
return () => sub.remove();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Doc 03 §3.4 — delegates to the native TurboModule, which resolves `keyId` to
|
|
140
|
+
* build-time-embedded key material (iOS CommonCrypto/CryptoKit, Android
|
|
141
|
+
* java.security/javax.crypto) and never exposes key material to JS.
|
|
142
|
+
*/
|
|
143
|
+
class RnCryptoProvider implements ICryptoProvider {
|
|
144
|
+
async verifyEd25519(
|
|
145
|
+
keyId: string,
|
|
146
|
+
messageBase64: string,
|
|
147
|
+
signatureBase64: string
|
|
148
|
+
): Promise<boolean> {
|
|
149
|
+
return NativeVersionCheck().verifyEd25519(
|
|
150
|
+
keyId,
|
|
151
|
+
messageBase64,
|
|
152
|
+
signatureBase64
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
async verifyHmacSha256(
|
|
156
|
+
keyId: string,
|
|
157
|
+
messageBase64: string,
|
|
158
|
+
macBase64: string
|
|
159
|
+
): Promise<boolean> {
|
|
160
|
+
return NativeVersionCheck().verifyHmacSha256(
|
|
161
|
+
keyId,
|
|
162
|
+
messageBase64,
|
|
163
|
+
macBase64
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export class NativePlatformBridge implements IPlatformBridge {
|
|
169
|
+
readonly platform: PlatformId = Platform.OS === 'ios' ? 'ios' : 'android';
|
|
170
|
+
readonly http: IHttpClient = new RnHttpClient();
|
|
171
|
+
readonly storage: IKeyValueStorage = new RnKeyValueStorage();
|
|
172
|
+
readonly secureStorage: ISecureStorage = new RnSecureStorage();
|
|
173
|
+
readonly appInfo: IAppInfoProvider = new RnAppInfoProvider();
|
|
174
|
+
readonly deviceInfo: IDeviceInfoProvider = new RnDeviceInfoProvider();
|
|
175
|
+
readonly scheduler: IBackgroundScheduler = new RnBackgroundScheduler();
|
|
176
|
+
readonly clock = { now: (): number => Date.now() };
|
|
177
|
+
readonly lifecycle: IAppLifecycleObserver = new RnAppLifecycleObserver();
|
|
178
|
+
readonly crypto: ICryptoProvider = new RnCryptoProvider();
|
|
179
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { TurboModuleRegistry, type TurboModule } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TurboModule Codegen spec. Deliberately small: HTTP (global `fetch`), the clock
|
|
5
|
+
* (`Date.now()`), and app-lifecycle (`AppState`) are already available in pure JS via
|
|
6
|
+
* React Native itself and do not need a custom native surface (Prompt 1 §4.1 revised
|
|
7
|
+
* during implementation — the design doc's per-target table listed a native `http`
|
|
8
|
+
* bridge conceptually, but RN's own Networking module already satisfies IHttpClient
|
|
9
|
+
* without new native code). What genuinely requires native code — reading the app's
|
|
10
|
+
* own version/bundle id and persisting key-value pairs via UserDefaults/Keychain or
|
|
11
|
+
* SharedPreferences/Keystore — is exactly what's declared here.
|
|
12
|
+
*/
|
|
13
|
+
export interface Spec extends TurboModule {
|
|
14
|
+
getCurrentAppVersion(): Promise<string>;
|
|
15
|
+
getBuildNumber(): Promise<string>;
|
|
16
|
+
getBundleId(): Promise<string>;
|
|
17
|
+
getOsVersion(): Promise<string>;
|
|
18
|
+
getDeviceModel(): Promise<string>;
|
|
19
|
+
getLocale(): Promise<string>;
|
|
20
|
+
|
|
21
|
+
storageGet(key: string): Promise<string | null>;
|
|
22
|
+
storageSet(key: string, value: string): Promise<void>;
|
|
23
|
+
storageRemove(key: string): Promise<void>;
|
|
24
|
+
|
|
25
|
+
secureStorageGet(key: string): Promise<string | null>;
|
|
26
|
+
secureStorageSet(key: string, value: string): Promise<void>;
|
|
27
|
+
secureStorageRemove(key: string): Promise<void>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Doc 03 §3.4 — signature verification over OS-provided cryptography. `keyId` is
|
|
31
|
+
* resolved to build-time-embedded key material natively (Info.plist/BuildConfig);
|
|
32
|
+
* only the keyId, not the key itself, crosses this bridge. Android implements both
|
|
33
|
+
* via java.security/javax.crypto; iOS implements HMAC-SHA256 via CommonCrypto and
|
|
34
|
+
* Ed25519 via a small CryptoKit-backed Swift wrapper (ios/PlatformBridge/Ed25519Verifier.swift).
|
|
35
|
+
*/
|
|
36
|
+
verifyEd25519(
|
|
37
|
+
keyId: string,
|
|
38
|
+
messageBase64: string,
|
|
39
|
+
signatureBase64: string
|
|
40
|
+
): Promise<boolean>;
|
|
41
|
+
verifyHmacSha256(
|
|
42
|
+
keyId: string,
|
|
43
|
+
messageBase64: string,
|
|
44
|
+
macBase64: string
|
|
45
|
+
): Promise<boolean>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let cachedModule: Spec | null = null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lazily resolved rather than the usual top-level
|
|
52
|
+
* `TurboModuleRegistry.getEnforcing<Spec>(...)` pattern, so importing this module (or
|
|
53
|
+
* anything that transitively imports it, e.g. src/di/container.ts) never throws in
|
|
54
|
+
* environments where the native module isn't registered — Jest's simulated native
|
|
55
|
+
* platform resolution, or a caller who supplies VersionManagerOptions.platformBridge
|
|
56
|
+
* and therefore never actually needs it. The registry lookup only happens the first
|
|
57
|
+
* time a method is actually called.
|
|
58
|
+
*/
|
|
59
|
+
export default function getNativeVersionCheck(): Spec {
|
|
60
|
+
if (!cachedModule) {
|
|
61
|
+
cachedModule = TurboModuleRegistry.getEnforcing<Spec>('VersionCheck');
|
|
62
|
+
}
|
|
63
|
+
return cachedModule;
|
|
64
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OfflineException,
|
|
3
|
+
RequestTimeoutException,
|
|
4
|
+
} from '../../domain/errors/VersionManagerException';
|
|
5
|
+
import type { PlatformId } from '../../domain/models/PlatformId';
|
|
6
|
+
import type {
|
|
7
|
+
HttpRequest,
|
|
8
|
+
HttpResponse,
|
|
9
|
+
IAppInfoProvider,
|
|
10
|
+
IAppLifecycleObserver,
|
|
11
|
+
IBackgroundScheduler,
|
|
12
|
+
ICryptoProvider,
|
|
13
|
+
IDeviceInfoProvider,
|
|
14
|
+
IHttpClient,
|
|
15
|
+
IKeyValueStorage,
|
|
16
|
+
IPlatformBridge,
|
|
17
|
+
ISecureStorage,
|
|
18
|
+
Unsubscribe,
|
|
19
|
+
BackgroundTaskDescriptor,
|
|
20
|
+
} from '../../domain/ports/IPlatformBridge';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* This project's tsconfig deliberately omits the "DOM" lib (it collides with React
|
|
24
|
+
* Native's own global fetch/console typings — Prompt 1 constraint). The small subset
|
|
25
|
+
* of browser globals this bridge needs is declared locally instead of pulling in the
|
|
26
|
+
* full lib.dom.d.ts surface, and accessed through a single typed cast of globalThis.
|
|
27
|
+
*/
|
|
28
|
+
interface WebFetchResponseLike {
|
|
29
|
+
readonly status: number;
|
|
30
|
+
readonly headers: { forEach(cb: (value: string, key: string) => void): void };
|
|
31
|
+
text(): Promise<string>;
|
|
32
|
+
}
|
|
33
|
+
type WebFetchInit = {
|
|
34
|
+
method?: string;
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
body?: string;
|
|
37
|
+
signal?: unknown;
|
|
38
|
+
};
|
|
39
|
+
type WebFetchLike = (
|
|
40
|
+
url: string,
|
|
41
|
+
init?: WebFetchInit
|
|
42
|
+
) => Promise<WebFetchResponseLike>;
|
|
43
|
+
|
|
44
|
+
interface WebStorageLike {
|
|
45
|
+
getItem(key: string): string | null;
|
|
46
|
+
setItem(key: string, value: string): void;
|
|
47
|
+
removeItem(key: string): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface WebNavigatorLike {
|
|
51
|
+
readonly language: string;
|
|
52
|
+
readonly userAgent: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface WebDocumentLike {
|
|
56
|
+
readonly visibilityState: string;
|
|
57
|
+
addEventListener(type: string, cb: () => void): void;
|
|
58
|
+
removeEventListener(type: string, cb: () => void): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface WebAbortControllerLike {
|
|
62
|
+
readonly signal: unknown;
|
|
63
|
+
abort(): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface WebSubtleCryptoLike {
|
|
67
|
+
importKey(
|
|
68
|
+
format: string,
|
|
69
|
+
keyData: unknown,
|
|
70
|
+
algorithm: unknown,
|
|
71
|
+
extractable: boolean,
|
|
72
|
+
keyUsages: string[]
|
|
73
|
+
): Promise<unknown>;
|
|
74
|
+
verify(
|
|
75
|
+
algorithm: unknown,
|
|
76
|
+
key: unknown,
|
|
77
|
+
signature: unknown,
|
|
78
|
+
data: unknown
|
|
79
|
+
): Promise<boolean>;
|
|
80
|
+
sign(algorithm: unknown, key: unknown, data: unknown): Promise<unknown>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface WebGlobals {
|
|
84
|
+
fetch: WebFetchLike;
|
|
85
|
+
localStorage: WebStorageLike;
|
|
86
|
+
navigator: WebNavigatorLike;
|
|
87
|
+
document: WebDocumentLike;
|
|
88
|
+
AbortController: new () => WebAbortControllerLike;
|
|
89
|
+
crypto?: { subtle: WebSubtleCryptoLike };
|
|
90
|
+
window?: {
|
|
91
|
+
addEventListener(type: string, cb: () => void): void;
|
|
92
|
+
removeEventListener(type: string, cb: () => void): void;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Doc 03 §3.4's "embedded at build time" tier, for Web: since there is no OS
|
|
96
|
+
* keychain/BuildConfig equivalent in a browser, the Web-target convention is a
|
|
97
|
+
* build-time `define` (Vite/webpack) injecting this global with keyId -> base64 key
|
|
98
|
+
* material — analogous to how §5.1 treats "Cache-Control: immutable" as the Web
|
|
99
|
+
* equivalent of native mmap. Absent this global, ed25519/hmac-sha256 verification on
|
|
100
|
+
* Web fails closed (returns false) rather than silently trusting an unverifiable doc.
|
|
101
|
+
*/
|
|
102
|
+
__VM_TRUSTED_KEYS__?: Record<string, string>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const webGlobal = globalThis as unknown as WebGlobals;
|
|
106
|
+
|
|
107
|
+
function base64ToBytes(base64: string): Uint8Array {
|
|
108
|
+
if (typeof atob === 'function') {
|
|
109
|
+
const binary = atob(base64);
|
|
110
|
+
const bytes = new Uint8Array(binary.length);
|
|
111
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
112
|
+
return bytes;
|
|
113
|
+
}
|
|
114
|
+
throw new Error('No base64 decoder available in this environment');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Doc 03 §3.4 crypto port, Web target — uses browser-native SubtleCrypto exclusively
|
|
119
|
+
* (Prompt 1 §9.3: no third-party crypto library). HMAC-SHA256 has broad browser
|
|
120
|
+
* support; Ed25519 support via WebCrypto is newer (Chrome 129+/Safari 17+) and this
|
|
121
|
+
* method throws a clear, caught error on unsupported browsers rather than silently
|
|
122
|
+
* failing — SignatureVerifier surfaces that as a normal SignatureVerificationException.
|
|
123
|
+
*/
|
|
124
|
+
class WebCryptoProvider implements ICryptoProvider {
|
|
125
|
+
async verifyEd25519(
|
|
126
|
+
keyId: string,
|
|
127
|
+
messageBase64: string,
|
|
128
|
+
signatureBase64: string
|
|
129
|
+
): Promise<boolean> {
|
|
130
|
+
const subtle = webGlobal.crypto?.subtle;
|
|
131
|
+
const keyMaterial = webGlobal.__VM_TRUSTED_KEYS__?.[keyId];
|
|
132
|
+
if (!subtle || !keyMaterial) return false;
|
|
133
|
+
const key = await subtle.importKey(
|
|
134
|
+
'raw',
|
|
135
|
+
base64ToBytes(keyMaterial),
|
|
136
|
+
{ name: 'Ed25519' },
|
|
137
|
+
false,
|
|
138
|
+
['verify']
|
|
139
|
+
);
|
|
140
|
+
return subtle.verify(
|
|
141
|
+
{ name: 'Ed25519' },
|
|
142
|
+
key,
|
|
143
|
+
base64ToBytes(signatureBase64),
|
|
144
|
+
base64ToBytes(messageBase64)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async verifyHmacSha256(
|
|
149
|
+
keyId: string,
|
|
150
|
+
messageBase64: string,
|
|
151
|
+
macBase64: string
|
|
152
|
+
): Promise<boolean> {
|
|
153
|
+
const subtle = webGlobal.crypto?.subtle;
|
|
154
|
+
const keyMaterial = webGlobal.__VM_TRUSTED_KEYS__?.[keyId];
|
|
155
|
+
if (!subtle || !keyMaterial) return false;
|
|
156
|
+
const key = await subtle.importKey(
|
|
157
|
+
'raw',
|
|
158
|
+
base64ToBytes(keyMaterial),
|
|
159
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
160
|
+
false,
|
|
161
|
+
['sign']
|
|
162
|
+
);
|
|
163
|
+
const computed = await subtle.sign(
|
|
164
|
+
{ name: 'HMAC' },
|
|
165
|
+
key,
|
|
166
|
+
base64ToBytes(messageBase64)
|
|
167
|
+
);
|
|
168
|
+
const computedBytes = new Uint8Array(computed as ArrayBuffer);
|
|
169
|
+
const expectedBytes = base64ToBytes(macBase64);
|
|
170
|
+
if (computedBytes.length !== expectedBytes.length) return false;
|
|
171
|
+
let diff = 0;
|
|
172
|
+
for (let i = 0; i < computedBytes.length; i++) {
|
|
173
|
+
diff |= (computedBytes[i] as number) ^ (expectedBytes[i] as number);
|
|
174
|
+
}
|
|
175
|
+
return diff === 0;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
class WebHttpClient implements IHttpClient {
|
|
180
|
+
async request(req: HttpRequest): Promise<HttpResponse> {
|
|
181
|
+
const controller = new webGlobal.AbortController();
|
|
182
|
+
const timeout = setTimeout(() => controller.abort(), req.timeoutMs);
|
|
183
|
+
try {
|
|
184
|
+
const response = await webGlobal.fetch(req.url, {
|
|
185
|
+
method: req.method,
|
|
186
|
+
headers: req.headers as Record<string, string> | undefined,
|
|
187
|
+
body: req.body,
|
|
188
|
+
signal: controller.signal,
|
|
189
|
+
});
|
|
190
|
+
const headers: Record<string, string> = {};
|
|
191
|
+
response.headers.forEach((value, key) => {
|
|
192
|
+
headers[key] = value;
|
|
193
|
+
});
|
|
194
|
+
const body = await response.text();
|
|
195
|
+
return { status: response.status, headers, body };
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
198
|
+
throw new RequestTimeoutException({
|
|
199
|
+
message: `Request to "${req.url}" timed out after ${req.timeoutMs}ms`,
|
|
200
|
+
cause: error,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
throw new OfflineException({
|
|
204
|
+
message: `Request to "${req.url}" failed`,
|
|
205
|
+
cause: error,
|
|
206
|
+
});
|
|
207
|
+
} finally {
|
|
208
|
+
clearTimeout(timeout);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
class WebKeyValueStorage implements IKeyValueStorage {
|
|
214
|
+
async get(key: string): Promise<string | null> {
|
|
215
|
+
return webGlobal.localStorage.getItem(key);
|
|
216
|
+
}
|
|
217
|
+
async set(key: string, value: string): Promise<void> {
|
|
218
|
+
webGlobal.localStorage.setItem(key, value);
|
|
219
|
+
}
|
|
220
|
+
async remove(key: string): Promise<void> {
|
|
221
|
+
webGlobal.localStorage.removeItem(key);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Backed by localStorage under a distinct key prefix. This is functionally complete
|
|
227
|
+
* (satisfies ISecureStorage) but is NOT encrypted at rest — true SubtleCrypto-based
|
|
228
|
+
* encryption-at-rest for the Web target (Prompt 1 §4.1's documented design) is a
|
|
229
|
+
* follow-up hardening item, not implemented in this pass.
|
|
230
|
+
*/
|
|
231
|
+
class WebSecureStorage implements ISecureStorage {
|
|
232
|
+
private static readonly PREFIX = 'vm_secure_';
|
|
233
|
+
async getItem(key: string): Promise<string | null> {
|
|
234
|
+
return webGlobal.localStorage.getItem(WebSecureStorage.PREFIX + key);
|
|
235
|
+
}
|
|
236
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
237
|
+
webGlobal.localStorage.setItem(WebSecureStorage.PREFIX + key, value);
|
|
238
|
+
}
|
|
239
|
+
async removeItem(key: string): Promise<void> {
|
|
240
|
+
webGlobal.localStorage.removeItem(WebSecureStorage.PREFIX + key);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The Web target has no OS bundle to read a version/build number from. These should be
|
|
246
|
+
* injected at build time (e.g. a Vite `define`); absent that, callers are expected to
|
|
247
|
+
* supply `VersionManagerOptions.appVersion` explicitly, which Core prefers over this
|
|
248
|
+
* bridge (see VersionManagerCore.resolveCurrentVersion()).
|
|
249
|
+
*/
|
|
250
|
+
class WebAppInfoProvider implements IAppInfoProvider {
|
|
251
|
+
async getCurrentVersion(): Promise<string> {
|
|
252
|
+
return '0.0.0';
|
|
253
|
+
}
|
|
254
|
+
async getBuildNumber(): Promise<string> {
|
|
255
|
+
return '0';
|
|
256
|
+
}
|
|
257
|
+
async getBundleId(): Promise<string> {
|
|
258
|
+
return 'web';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
class WebDeviceInfoProvider implements IDeviceInfoProvider {
|
|
263
|
+
async getOsVersion(): Promise<string> {
|
|
264
|
+
return webGlobal.navigator.userAgent;
|
|
265
|
+
}
|
|
266
|
+
async getDeviceModel(): Promise<string> {
|
|
267
|
+
return 'web';
|
|
268
|
+
}
|
|
269
|
+
async getLocale(): Promise<string> {
|
|
270
|
+
return webGlobal.navigator.language;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* True Service Worker `periodicSync` requires a registered service worker the host app
|
|
276
|
+
* controls (feature-detected, narrow browser support) and is out of this pass's scope —
|
|
277
|
+
* see the class-level fallback doc 01 §4.1 explicitly names: "visibility-change
|
|
278
|
+
* fallback." This implements exactly that fallback for real: a `setInterval` gated so
|
|
279
|
+
* the callback only actually fires while the tab is visible (`document.visibilityState
|
|
280
|
+
* === 'visible'`), which is the best a page can do without its own registered SW.
|
|
281
|
+
* `minIntervalMs` is honored as a floor, not a guarantee — like all browser timers, it
|
|
282
|
+
* is throttled/paused while backgrounded, which is the whole reason for the visibility
|
|
283
|
+
* gate (avoid firing a burst of missed intervals the instant the tab regains focus).
|
|
284
|
+
*/
|
|
285
|
+
class WebBackgroundScheduler implements IBackgroundScheduler {
|
|
286
|
+
private readonly timers = new Map<string, ReturnType<typeof setInterval>>();
|
|
287
|
+
private readonly callbacks = new Map<string, () => void>();
|
|
288
|
+
|
|
289
|
+
async schedule(task: BackgroundTaskDescriptor): Promise<void> {
|
|
290
|
+
this.cancelInternal(task.taskId);
|
|
291
|
+
const timer = setInterval(() => {
|
|
292
|
+
if (webGlobal.document.visibilityState !== 'visible') return;
|
|
293
|
+
this.callbacks.get(task.taskId)?.();
|
|
294
|
+
}, task.minIntervalMs);
|
|
295
|
+
this.timers.set(task.taskId, timer);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async cancel(taskId: string): Promise<void> {
|
|
299
|
+
this.cancelInternal(taskId);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Registers the function a scheduled task actually invokes when it fires (wired by src/di/container.ts, not part of IBackgroundScheduler's public port shape). */
|
|
303
|
+
setCallback(taskId: string, cb: () => void): void {
|
|
304
|
+
this.callbacks.set(taskId, cb);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private cancelInternal(taskId: string): void {
|
|
308
|
+
const existing = this.timers.get(taskId);
|
|
309
|
+
if (existing !== undefined) {
|
|
310
|
+
clearInterval(existing);
|
|
311
|
+
this.timers.delete(taskId);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
class WebAppLifecycleObserver implements IAppLifecycleObserver {
|
|
317
|
+
onForeground(cb: () => void): Unsubscribe {
|
|
318
|
+
const handler = (): void => {
|
|
319
|
+
if (webGlobal.document.visibilityState === 'visible') cb();
|
|
320
|
+
};
|
|
321
|
+
webGlobal.document.addEventListener('visibilitychange', handler);
|
|
322
|
+
return () =>
|
|
323
|
+
webGlobal.document.removeEventListener('visibilitychange', handler);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
onBackground(cb: () => void): Unsubscribe {
|
|
327
|
+
const handler = (): void => {
|
|
328
|
+
if (webGlobal.document.visibilityState === 'hidden') cb();
|
|
329
|
+
};
|
|
330
|
+
webGlobal.document.addEventListener('visibilitychange', handler);
|
|
331
|
+
return () =>
|
|
332
|
+
webGlobal.document.removeEventListener('visibilitychange', handler);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export class WebPlatformBridge implements IPlatformBridge {
|
|
337
|
+
readonly platform: PlatformId = 'web';
|
|
338
|
+
readonly http: IHttpClient = new WebHttpClient();
|
|
339
|
+
readonly storage: IKeyValueStorage = new WebKeyValueStorage();
|
|
340
|
+
readonly secureStorage: ISecureStorage = new WebSecureStorage();
|
|
341
|
+
readonly appInfo: IAppInfoProvider = new WebAppInfoProvider();
|
|
342
|
+
readonly deviceInfo: IDeviceInfoProvider = new WebDeviceInfoProvider();
|
|
343
|
+
readonly scheduler: IBackgroundScheduler = new WebBackgroundScheduler();
|
|
344
|
+
readonly clock = { now: (): number => Date.now() };
|
|
345
|
+
readonly lifecycle: IAppLifecycleObserver = new WebAppLifecycleObserver();
|
|
346
|
+
readonly crypto: ICryptoProvider = new WebCryptoProvider();
|
|
347
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Modal, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
2
|
+
import type { ReactElement } from 'react';
|
|
3
|
+
import type { UpdateInfo } from '../../domain/models/UpdateInfo';
|
|
4
|
+
|
|
5
|
+
export interface ForceUpdateScreenProps {
|
|
6
|
+
updateInfo: UpdateInfo;
|
|
7
|
+
onUpdatePress: () => void;
|
|
8
|
+
title?: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
updateButtonLabel?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Full-screen, non-dismissible lockout (Prompt 1 §5.3; UI copy detail is Prompt 19). */
|
|
14
|
+
export function ForceUpdateScreen({
|
|
15
|
+
updateInfo,
|
|
16
|
+
onUpdatePress,
|
|
17
|
+
title = 'Update Required',
|
|
18
|
+
message = `Version ${updateInfo.latestVersion} is available. You must update to continue.`,
|
|
19
|
+
updateButtonLabel = 'Update Now',
|
|
20
|
+
}: ForceUpdateScreenProps): ReactElement {
|
|
21
|
+
return (
|
|
22
|
+
<Modal
|
|
23
|
+
visible
|
|
24
|
+
animationType="fade"
|
|
25
|
+
transparent={false}
|
|
26
|
+
onRequestClose={() => undefined}
|
|
27
|
+
>
|
|
28
|
+
<View style={styles.container}>
|
|
29
|
+
<Text style={styles.title}>{title}</Text>
|
|
30
|
+
<Text style={styles.message}>{message}</Text>
|
|
31
|
+
<Pressable
|
|
32
|
+
style={styles.button}
|
|
33
|
+
onPress={onUpdatePress}
|
|
34
|
+
accessibilityRole="button"
|
|
35
|
+
>
|
|
36
|
+
<Text style={styles.buttonLabel}>{updateButtonLabel}</Text>
|
|
37
|
+
</Pressable>
|
|
38
|
+
</View>
|
|
39
|
+
</Modal>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const styles = StyleSheet.create({
|
|
44
|
+
container: {
|
|
45
|
+
flex: 1,
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
padding: 24,
|
|
49
|
+
},
|
|
50
|
+
title: {
|
|
51
|
+
fontSize: 20,
|
|
52
|
+
fontWeight: '700',
|
|
53
|
+
marginBottom: 12,
|
|
54
|
+
textAlign: 'center',
|
|
55
|
+
},
|
|
56
|
+
message: { fontSize: 15, textAlign: 'center', marginBottom: 24 },
|
|
57
|
+
button: {
|
|
58
|
+
paddingVertical: 12,
|
|
59
|
+
paddingHorizontal: 24,
|
|
60
|
+
borderRadius: 8,
|
|
61
|
+
backgroundColor: '#2563eb',
|
|
62
|
+
},
|
|
63
|
+
buttonLabel: { color: '#ffffff', fontSize: 16, fontWeight: '600' },
|
|
64
|
+
});
|