@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
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rajesh Shinde
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 📦 @rs-native-kit/version-check
|
|
4
|
+
|
|
5
|
+
**Drop-in version-check & force/soft-update orchestration for React Native**
|
|
6
|
+
|
|
7
|
+
Detect when your users are behind, decide what to do about it, and ship the UI to tell them — all from one config call.
|
|
8
|
+
|
|
9
|
+
[](https://github.com/rajssinde/rs-native-kit-version-check/actions/workflows/ci.yml)
|
|
10
|
+
[](./LICENSE)
|
|
11
|
+
[](#)
|
|
12
|
+
[](#requirements)
|
|
13
|
+
[](#requirements)
|
|
14
|
+
[](#)
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Why this exists
|
|
21
|
+
|
|
22
|
+
Shipping an update means nothing if half your users are stuck on a version from three releases ago. **`version-check`** answers three questions for you on every app launch:
|
|
23
|
+
|
|
24
|
+
1. **Is the user out of date?** — compares the running app version against what your store (or your own API) says is current.
|
|
25
|
+
2. **How out of date, and does it matter?** — runs that comparison through a policy engine (hard floor, reminder cadence, staged rollout %) to decide what actually needs to happen.
|
|
26
|
+
3. **What should the UI do about it?** — hands back a typed `ActionPlan` (`FORCE_UPDATE`, `SOFT_UPDATE`, `OPTIONAL_REMINDER`, `NO_ACTION`) that you render with the included components, your own UI, or ignore entirely if you just want the raw decision.
|
|
27
|
+
|
|
28
|
+
## At a glance
|
|
29
|
+
|
|
30
|
+
| | |
|
|
31
|
+
|---|---|
|
|
32
|
+
| 📦 Runtime dependencies | **0** — nothing pulled in at install time |
|
|
33
|
+
| 🏬 Store providers | Apple App Store, Google Play, Custom API (Huawei/Amazon/Firebase planned) |
|
|
34
|
+
| ⏱️ Default check cache | 6h TTL, persisted across app restarts |
|
|
35
|
+
| 🔁 Default reminder cadence | Every 3 days after "Later" |
|
|
36
|
+
| 🎯 Staged rollout | 0–100% device-bucketed rollout out of the box |
|
|
37
|
+
| 🧩 Setup | One `configure()` call, or `<VersionManagerProvider>` + a hook |
|
|
38
|
+
| 🖼️ Prebuilt UI | 3 ready-made components, or go fully headless |
|
|
39
|
+
| 🧪 Test coverage | Domain/config logic covered by unit tests, no device required |
|
|
40
|
+
| 🌐 Platforms | iOS, Android, Web (`react-native-web`) |
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- 🏬 **Multi-store version lookup** — App Store, Google Play, or your own Custom API out of the box.
|
|
45
|
+
- 🧠 **Policy engine** — SemVer-aware force-update floor, reminder throttling, and percentage-based staged rollout.
|
|
46
|
+
- 🔐 **Signed remote config** — optionally hot-load update policy from a URL or bundled JSON, verified with Ed25519/HMAC via native OS crypto (never hand-rolled in JS).
|
|
47
|
+
- ⚛️ **React hooks + prebuilt UI** — `useVersionManager`, `<ForceUpdateScreen />`, `<SoftUpdateDialog />`, `<OptionalUpdateBanner />` — or go headless and build your own.
|
|
48
|
+
- 📡 **Event-driven** — subscribe to `stateChanged`, `updateDetected`, `userAction`, and `error` instead of polling.
|
|
49
|
+
- 🌐 **Web-ready** — the same JS API runs under `react-native-web`; no separate SDK.
|
|
50
|
+
- 🪶 **Zero runtime dependencies** — the entire JS surface is hand-written, nothing pulled in at install time.
|
|
51
|
+
- 🧪 **Fully unit-tested domain core** — SemVer engine, policy engine, lifecycle state machine, and config pipeline all run against mocks, no native layer required.
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
This library ships as a **TurboModule** with no legacy-bridge fallback, so the New Architecture must be enabled.
|
|
56
|
+
|
|
57
|
+
| Requirement | Minimum | Tested against |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| React Native | **0.74+**, New Architecture (Fabric/TurboModules) enabled | 0.85.0 |
|
|
60
|
+
| React | 18+ | 19.2.3 |
|
|
61
|
+
| iOS | **15.1+** | Xcode 16.1+ |
|
|
62
|
+
| Android | **API 24** (Android 7.0)+ | compileSdk 36, Kotlin 2.0.21, JDK 17 |
|
|
63
|
+
| Node.js | 20+ | 24.13.0 (see `.nvmrc`) |
|
|
64
|
+
| TypeScript | 5+ (optional, but the package ships full `.d.ts`) | 6.0.3 |
|
|
65
|
+
|
|
66
|
+
| Platform | Status |
|
|
67
|
+
|---|---|
|
|
68
|
+
| iOS | ✅ Native TurboModule (`ios/VersionCheck.mm`, Ed25519 verification) |
|
|
69
|
+
| Android | ✅ Native TurboModule (Kotlin, WorkManager-backed background scheduling) |
|
|
70
|
+
| Web (`react-native-web`) | ✅ Same JS API, browser-native `fetch`/`localStorage` bridge |
|
|
71
|
+
|
|
72
|
+
## Installation
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
npm install @rs-native-kit/version-check
|
|
76
|
+
# or
|
|
77
|
+
yarn add @rs-native-kit/version-check
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**iOS** — install pods after adding the package:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
cd ios && pod install
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Android** — no extra steps; the TurboModule autolinks via `react-native.config.js`.
|
|
87
|
+
|
|
88
|
+
## Quick start
|
|
89
|
+
|
|
90
|
+
### Core API (framework-agnostic, zero React code pulled in)
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { VersionManager } from '@rs-native-kit/version-check';
|
|
94
|
+
|
|
95
|
+
const manager = VersionManager.configure({
|
|
96
|
+
stores: {
|
|
97
|
+
ios: { appStoreId: '123456789' }, // optional — omit to fall back to this app's bundle id
|
|
98
|
+
android: { packageName: 'com.example.app' }, // optional — omit to use this app's own package name
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await manager.ready(); // waits for config resolution to settle
|
|
103
|
+
const plan = await manager.checkForUpdates();
|
|
104
|
+
|
|
105
|
+
switch (plan.type) {
|
|
106
|
+
case 'FORCE_UPDATE':
|
|
107
|
+
// block the app, show ForceUpdateScreen
|
|
108
|
+
break;
|
|
109
|
+
case 'SOFT_UPDATE':
|
|
110
|
+
// dismissible prompt, show SoftUpdateDialog
|
|
111
|
+
break;
|
|
112
|
+
case 'OPTIONAL_REMINDER':
|
|
113
|
+
// low-intrusion nudge, show OptionalUpdateBanner
|
|
114
|
+
break;
|
|
115
|
+
case 'NO_ACTION':
|
|
116
|
+
// already current
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### React hooks + prebuilt UI (separate, tree-shakeable entry point)
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import {
|
|
125
|
+
useVersionManager,
|
|
126
|
+
ForceUpdateScreen,
|
|
127
|
+
SoftUpdateDialog,
|
|
128
|
+
OptionalUpdateBanner,
|
|
129
|
+
} from '@rs-native-kit/version-check/ui';
|
|
130
|
+
import { Linking } from 'react-native';
|
|
131
|
+
|
|
132
|
+
function App() {
|
|
133
|
+
const { state, isUpdateAvailable, updateInfo, checkForUpdates } =
|
|
134
|
+
useVersionManager({
|
|
135
|
+
stores: {
|
|
136
|
+
ios: { appStoreId: '123456789' },
|
|
137
|
+
android: { packageName: 'com.example.app' },
|
|
138
|
+
},
|
|
139
|
+
policy: {
|
|
140
|
+
forceUpdateBelow: '2.0.0',
|
|
141
|
+
reminderIntervalMs: 3 * 24 * 60 * 60 * 1000, // 3 days
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (updateInfo?.isForceUpdate) {
|
|
146
|
+
return (
|
|
147
|
+
<ForceUpdateScreen
|
|
148
|
+
updateInfo={updateInfo}
|
|
149
|
+
onUpdatePress={() => Linking.openURL(updateInfo.storeUrl)}
|
|
150
|
+
/>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<>
|
|
156
|
+
{/* your app */}
|
|
157
|
+
{isUpdateAvailable && updateInfo && (
|
|
158
|
+
<SoftUpdateDialog
|
|
159
|
+
updateInfo={updateInfo}
|
|
160
|
+
onUpdatePress={() => Linking.openURL(updateInfo.storeUrl)}
|
|
161
|
+
onLaterPress={() => {}}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
</>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Events
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
manager.onUpdateDetected(({ updateInfo, actionPlan }) => { /* ... */ });
|
|
173
|
+
manager.onUserAction(({ action, updateInfo }) => { /* 'update_clicked' | 'later_clicked' | ... */ });
|
|
174
|
+
manager.onStateChanged(({ from, to }) => { /* lifecycle transitions */ });
|
|
175
|
+
manager.onError(({ error, phase }) => { /* 'config' | 'check' | 'policy' | 'presentation' */ });
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Configuration reference
|
|
179
|
+
|
|
180
|
+
Every field below is optional except `stores`, which needs at least one provider configured.
|
|
181
|
+
|
|
182
|
+
`stores.ios.appStoreId` and `stores.android.packageName` are themselves optional — pass them when you have them (App Store lookups by numeric id are the most reliable), and omit them to fall back automatically to the running app's own bundle id (`IAppInfoProvider.getBundleId()`, resolved at check time) — which *is* the package name on Android, and a supported alternate lookup key (`bundleId=`) against the iTunes Lookup API on iOS:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
VersionManager.configure({
|
|
186
|
+
stores: {
|
|
187
|
+
ios: { region: 'us' }, // appStoreId omitted -> looked up by this app's bundle id
|
|
188
|
+
android: {}, // packageName omitted -> uses this app's own package name
|
|
189
|
+
custom: { url: 'https://api.example.com/version.json', headers: {} },
|
|
190
|
+
},
|
|
191
|
+
policy: {
|
|
192
|
+
forceUpdateBelow: '2.0.0', // SemVer floor — anything older is FORCE_UPDATE
|
|
193
|
+
reminderIntervalMs: 259_200_000, // how often to re-nag after "Later" (default 3 days)
|
|
194
|
+
rolloutPercentage: 100, // staged rollout, 0-100
|
|
195
|
+
},
|
|
196
|
+
cache: {
|
|
197
|
+
ttlMs: 21_600_000, // how long a store lookup is cached (default 6h)
|
|
198
|
+
storage: 'persistent', // 'memory' | 'persistent'
|
|
199
|
+
bustOnManualCheck: true,
|
|
200
|
+
},
|
|
201
|
+
fallback: {
|
|
202
|
+
onNetworkError: 'useCache', // 'useCache' | 'useDefaultConfig' | 'noAction'
|
|
203
|
+
requestTimeoutMs: 8_000,
|
|
204
|
+
retry: { maxAttempts: 3, backoff: 'exponential-jitter', baseDelayMs: 500 },
|
|
205
|
+
},
|
|
206
|
+
remoteConfigUrl: 'https://api.example.com/vm-config.json', // optional hot-reloadable config
|
|
207
|
+
security: { signatureAlgorithm: 'ed25519', trustedKeyIds: ['prod-key-1'] },
|
|
208
|
+
logging: { level: 'warn' },
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Store providers
|
|
213
|
+
|
|
214
|
+
| Provider | Status |
|
|
215
|
+
|---|---|
|
|
216
|
+
| Apple App Store | ✅ Implemented |
|
|
217
|
+
| Google Play | ✅ Implemented |
|
|
218
|
+
| Custom API | ✅ Implemented |
|
|
219
|
+
| Huawei AppGallery | 🚧 Planned — throws `UnsupportedStoreException` today |
|
|
220
|
+
| Amazon Appstore | 🚧 Planned — throws `UnsupportedStoreException` today |
|
|
221
|
+
| Firebase Remote Config | 🚧 Planned — throws `UnsupportedStoreException` today |
|
|
222
|
+
|
|
223
|
+
### Remote / signed configuration
|
|
224
|
+
|
|
225
|
+
On top of the options you hardcode at `configure()`, you can layer a **local bundled JSON** and/or a **remote URL**, both wrapped in a signed envelope (`schemaVersion`, `payload`, `signature`) and verified against `security.trustedKeyIds` before any field is trusted. Precedence is `env overrides > remote > local > configure() defaults`, merged field-by-field, with automatic fallback to the last known-good config if a remote fetch fails.
|
|
226
|
+
|
|
227
|
+
## Lifecycle
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
UNINITIALIZED → CONFIG_LOADING → IDLE ⇄ VERSION_CHECKING → DECIDING
|
|
231
|
+
│
|
|
232
|
+
┌─────────────────┬───────────────────┼───────────────────┐
|
|
233
|
+
▼ ▼ ▼ ▼
|
|
234
|
+
FORCE_UPDATE_DISPLAYED SOFT_UPDATE_DISPLAYED REJECTED_WAITING_REMINDER IDLE
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Read the current state any time with `manager.getCurrentState()`, or subscribe via `onStateChanged`.
|
|
238
|
+
|
|
239
|
+
## Example app
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
yarn
|
|
243
|
+
yarn example ios # or `yarn example android`, `yarn example web`
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Contributing
|
|
247
|
+
|
|
248
|
+
- [Development workflow](CONTRIBUTING.md#development-workflow)
|
|
249
|
+
- [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
|
|
250
|
+
- [Code of conduct](CODE_OF_CONDUCT.md)
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT © [Rajesh Shinde](https://github.com/rajssinde)
|
|
255
|
+
|
|
256
|
+
---
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "VersionCheck"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["homepage"]
|
|
10
|
+
s.license = package["license"]
|
|
11
|
+
s.authors = package["author"]
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
14
|
+
s.source = { :git => "https://github.com/rajssinde/rs-native-kit-version-check.git", :tag => "#{s.version}" }
|
|
15
|
+
|
|
16
|
+
s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
|
|
17
|
+
s.private_header_files = "ios/**/*.h"
|
|
18
|
+
|
|
19
|
+
install_modules_dependencies(s)
|
|
20
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.VersionCheck = [
|
|
3
|
+
kotlinVersion: "2.0.21",
|
|
4
|
+
minSdkVersion: 24,
|
|
5
|
+
compileSdkVersion: 36
|
|
6
|
+
]
|
|
7
|
+
|
|
8
|
+
ext.getExtOrDefault = { prop ->
|
|
9
|
+
if (rootProject.ext.has(prop)) {
|
|
10
|
+
return rootProject.ext.get(prop)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return VersionCheck[prop]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
repositories {
|
|
17
|
+
google()
|
|
18
|
+
mavenCentral()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
dependencies {
|
|
22
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
23
|
+
// noinspection DifferentKotlinGradleVersion
|
|
24
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
apply plugin: "com.android.library"
|
|
30
|
+
apply plugin: "kotlin-android"
|
|
31
|
+
|
|
32
|
+
apply plugin: "com.facebook.react"
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace "com.rsnativekit.versioncheck"
|
|
36
|
+
|
|
37
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion")
|
|
38
|
+
|
|
39
|
+
defaultConfig {
|
|
40
|
+
minSdkVersion getExtOrDefault("minSdkVersion")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
compileOptions {
|
|
44
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
45
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
dependencies {
|
|
50
|
+
implementation "com.facebook.react:react-android"
|
|
51
|
+
// Backs the scheduleBackgroundCheck/cancelBackgroundCheck TurboModule methods
|
|
52
|
+
// (doc 01 §4.1 "scheduler" row, Android WorkManager) — an internal implementation
|
|
53
|
+
// detail of this library's own native module; consuming apps do not need to declare
|
|
54
|
+
// this themselves (it is bundled at runtime via normal AAR dependency resolution).
|
|
55
|
+
implementation "androidx.work:work-runtime-ktx:2.9.1"
|
|
56
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<!-- Doc 01 §4.1/§10 background-scheduler headless JS bridge (Prompt 26). Merged
|
|
3
|
+
into the host app's manifest by Gradle's normal library-manifest merge. -->
|
|
4
|
+
<application>
|
|
5
|
+
<service
|
|
6
|
+
android:name=".VersionCheckHeadlessTaskService"
|
|
7
|
+
android:exported="false" />
|
|
8
|
+
</application>
|
|
9
|
+
</manifest>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package com.rsnativekit.versioncheck
|
|
2
|
+
|
|
3
|
+
import android.security.keystore.KeyGenParameterSpec
|
|
4
|
+
import android.security.keystore.KeyProperties
|
|
5
|
+
import android.util.Base64
|
|
6
|
+
import java.security.KeyStore
|
|
7
|
+
import javax.crypto.Cipher
|
|
8
|
+
import javax.crypto.KeyGenerator
|
|
9
|
+
import javax.crypto.SecretKey
|
|
10
|
+
import javax.crypto.spec.GCMParameterSpec
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AES/GCM encryption backed by the Android Keystore — java.security/javax.crypto only,
|
|
14
|
+
* no external Gradle dependency (docs/architecture/03-configuration-system.md §3.4).
|
|
15
|
+
* The key is generated inside the keystore and never leaves it; only ciphertext + IV
|
|
16
|
+
* are persisted in SharedPreferences by the caller.
|
|
17
|
+
*/
|
|
18
|
+
internal object SecureStorageCipher {
|
|
19
|
+
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
|
|
20
|
+
private const val KEY_ALIAS = "rs_version_check_secure_storage_key"
|
|
21
|
+
private const val TRANSFORMATION = "AES/GCM/NoPadding"
|
|
22
|
+
private const val GCM_TAG_LENGTH_BITS = 128
|
|
23
|
+
|
|
24
|
+
private fun getOrCreateKey(): SecretKey {
|
|
25
|
+
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }
|
|
26
|
+
(keyStore.getKey(KEY_ALIAS, null) as? SecretKey)?.let { return it }
|
|
27
|
+
|
|
28
|
+
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER)
|
|
29
|
+
val spec =
|
|
30
|
+
KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
|
|
31
|
+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
|
32
|
+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
|
33
|
+
.build()
|
|
34
|
+
keyGenerator.init(spec)
|
|
35
|
+
return keyGenerator.generateKey()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fun encrypt(plainText: String): String {
|
|
39
|
+
val cipher = Cipher.getInstance(TRANSFORMATION)
|
|
40
|
+
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
|
|
41
|
+
val cipherBytes = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
|
|
42
|
+
val iv = Base64.encodeToString(cipher.iv, Base64.NO_WRAP)
|
|
43
|
+
val payload = Base64.encodeToString(cipherBytes, Base64.NO_WRAP)
|
|
44
|
+
return "$iv:$payload"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fun decrypt(stored: String): String {
|
|
48
|
+
val parts = stored.split(":", limit = 2)
|
|
49
|
+
require(parts.size == 2) { "Malformed secure storage entry" }
|
|
50
|
+
val iv = Base64.decode(parts[0], Base64.NO_WRAP)
|
|
51
|
+
val cipherBytes = Base64.decode(parts[1], Base64.NO_WRAP)
|
|
52
|
+
|
|
53
|
+
val cipher = Cipher.getInstance(TRANSFORMATION)
|
|
54
|
+
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv))
|
|
55
|
+
return String(cipher.doFinal(cipherBytes), Charsets.UTF_8)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
package com.rsnativekit.versioncheck
|
|
2
|
+
|
|
3
|
+
import android.content.Intent
|
|
4
|
+
import com.facebook.react.HeadlessJsTaskService
|
|
5
|
+
import com.facebook.react.bridge.Arguments
|
|
6
|
+
import com.facebook.react.jstasks.HeadlessJsTaskConfig
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Doc 01 §4.1/§10 "Headless JS task + native WorkManager/BGTaskScheduler bridge" — the
|
|
10
|
+
* Android half. VersionCheckWorker (WorkManager) starts this service when a scheduled
|
|
11
|
+
* check fires; this service starts the "VersionCheckBackgroundTask" headless JS task,
|
|
12
|
+
* which the host app must register once via the `registerVersionCheckHeadlessTask()`
|
|
13
|
+
* helper exported from '@rs-native-kit/version-check' (same integration shape as
|
|
14
|
+
* react-native-background-fetch and similar libraries require).
|
|
15
|
+
*/
|
|
16
|
+
class VersionCheckHeadlessTaskService : HeadlessJsTaskService() {
|
|
17
|
+
override fun getTaskConfig(intent: Intent): HeadlessJsTaskConfig? {
|
|
18
|
+
val extras = intent.extras
|
|
19
|
+
return HeadlessJsTaskConfig(
|
|
20
|
+
TASK_NAME,
|
|
21
|
+
if (extras != null) Arguments.fromBundle(extras) else Arguments.createMap(),
|
|
22
|
+
TASK_TIMEOUT_MS,
|
|
23
|
+
true
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
companion object {
|
|
28
|
+
const val TASK_NAME = "VersionCheckBackgroundTask"
|
|
29
|
+
const val TASK_TIMEOUT_MS = 30_000L
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
package com.rsnativekit.versioncheck
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import android.util.Base64
|
|
7
|
+
import androidx.work.ExistingPeriodicWorkPolicy
|
|
8
|
+
import androidx.work.PeriodicWorkRequestBuilder
|
|
9
|
+
import androidx.work.WorkManager
|
|
10
|
+
import com.facebook.react.bridge.Promise
|
|
11
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
12
|
+
import java.security.Signature
|
|
13
|
+
import java.util.Locale
|
|
14
|
+
import java.util.concurrent.TimeUnit
|
|
15
|
+
import javax.crypto.Mac
|
|
16
|
+
import javax.crypto.spec.SecretKeySpec
|
|
17
|
+
|
|
18
|
+
class VersionCheckModule(reactContext: ReactApplicationContext) :
|
|
19
|
+
NativeVersionCheckSpec(reactContext) {
|
|
20
|
+
|
|
21
|
+
private val prefs: SharedPreferences by lazy {
|
|
22
|
+
reactApplicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
23
|
+
}
|
|
24
|
+
private val securePrefs: SharedPreferences by lazy {
|
|
25
|
+
reactApplicationContext.getSharedPreferences(SECURE_PREFS_NAME, Context.MODE_PRIVATE)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// --- App info ---
|
|
29
|
+
|
|
30
|
+
override fun getCurrentAppVersion(promise: Promise) {
|
|
31
|
+
try {
|
|
32
|
+
val info = reactApplicationContext.packageManager.getPackageInfo(reactApplicationContext.packageName, 0)
|
|
33
|
+
promise.resolve(info.versionName ?: "")
|
|
34
|
+
} catch (error: Exception) {
|
|
35
|
+
promise.reject("get_current_app_version_failed", error)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun getBuildNumber(promise: Promise) {
|
|
40
|
+
try {
|
|
41
|
+
val info = reactApplicationContext.packageManager.getPackageInfo(reactApplicationContext.packageName, 0)
|
|
42
|
+
val code = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) info.longVersionCode else info.versionCode.toLong()
|
|
43
|
+
promise.resolve(code.toString())
|
|
44
|
+
} catch (error: Exception) {
|
|
45
|
+
promise.reject("get_build_number_failed", error)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override fun getBundleId(promise: Promise) {
|
|
50
|
+
promise.resolve(reactApplicationContext.packageName)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// --- Device info ---
|
|
54
|
+
|
|
55
|
+
override fun getOsVersion(promise: Promise) {
|
|
56
|
+
promise.resolve(Build.VERSION.RELEASE ?: Build.VERSION.SDK_INT.toString())
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override fun getDeviceModel(promise: Promise) {
|
|
60
|
+
promise.resolve("${Build.MANUFACTURER} ${Build.MODEL}")
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
override fun getLocale(promise: Promise) {
|
|
64
|
+
promise.resolve(Locale.getDefault().toLanguageTag())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Key-value storage (SharedPreferences) ---
|
|
68
|
+
|
|
69
|
+
override fun storageGet(key: String, promise: Promise) {
|
|
70
|
+
promise.resolve(prefs.getString(key, null))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override fun storageSet(key: String, value: String, promise: Promise) {
|
|
74
|
+
prefs.edit().putString(key, value).apply()
|
|
75
|
+
promise.resolve(null)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override fun storageRemove(key: String, promise: Promise) {
|
|
79
|
+
prefs.edit().remove(key).apply()
|
|
80
|
+
promise.resolve(null)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Secure storage (Keystore-backed AES/GCM over SharedPreferences) ---
|
|
84
|
+
|
|
85
|
+
override fun secureStorageGet(key: String, promise: Promise) {
|
|
86
|
+
try {
|
|
87
|
+
val stored = securePrefs.getString(key, null)
|
|
88
|
+
if (stored == null) {
|
|
89
|
+
promise.resolve(null)
|
|
90
|
+
} else {
|
|
91
|
+
promise.resolve(SecureStorageCipher.decrypt(stored))
|
|
92
|
+
}
|
|
93
|
+
} catch (error: Exception) {
|
|
94
|
+
promise.reject("secure_storage_get_failed", error)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
override fun secureStorageSet(key: String, value: String, promise: Promise) {
|
|
99
|
+
try {
|
|
100
|
+
securePrefs.edit().putString(key, SecureStorageCipher.encrypt(value)).apply()
|
|
101
|
+
promise.resolve(null)
|
|
102
|
+
} catch (error: Exception) {
|
|
103
|
+
promise.reject("secure_storage_set_failed", error)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
override fun secureStorageRemove(key: String, promise: Promise) {
|
|
108
|
+
securePrefs.edit().remove(key).apply()
|
|
109
|
+
promise.resolve(null)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// --- Signature verification (doc 03 §3.4) ---
|
|
113
|
+
//
|
|
114
|
+
// Trusted key material is embedded at build time via a Gradle-injected BuildConfig
|
|
115
|
+
// field (TRUSTED_SIGNING_KEYS_JSON, a JSON object mapping keyId -> base64 key
|
|
116
|
+
// material) rather than shipped from JS, per §3.4's "embedded at build time, not
|
|
117
|
+
// hardcoded in the JS bundle" requirement. Host apps supply this via
|
|
118
|
+
// `android.defaultConfig.buildConfigField "String", "TRUSTED_SIGNING_KEYS_JSON", "..."`
|
|
119
|
+
// in their own build.gradle; if absent, verification fails closed (returns false)
|
|
120
|
+
// rather than silently trusting an unverifiable document.
|
|
121
|
+
|
|
122
|
+
private fun resolveKeyMaterial(keyId: String): ByteArray? {
|
|
123
|
+
return try {
|
|
124
|
+
val field = Class.forName("${reactApplicationContext.packageName}.BuildConfig")
|
|
125
|
+
.getField("TRUSTED_SIGNING_KEYS_JSON")
|
|
126
|
+
val json = org.json.JSONObject(field.get(null) as String)
|
|
127
|
+
if (!json.has(keyId)) null else Base64.decode(json.getString(keyId), Base64.NO_WRAP)
|
|
128
|
+
} catch (error: Exception) {
|
|
129
|
+
null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
override fun verifyEd25519(keyId: String, messageBase64: String, signatureBase64: String, promise: Promise) {
|
|
134
|
+
try {
|
|
135
|
+
val keyMaterial = resolveKeyMaterial(keyId)
|
|
136
|
+
if (keyMaterial == null) {
|
|
137
|
+
promise.resolve(false)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
// Ed25519 via java.security requires API 33+ (Conscrypt) on stock Android; on
|
|
141
|
+
// older API levels this throws NoSuchAlgorithmException and verification fails
|
|
142
|
+
// closed rather than silently succeeding — documented platform-version gap.
|
|
143
|
+
val keySpec = java.security.spec.X509EncodedKeySpec(keyMaterial)
|
|
144
|
+
val keyFactory = java.security.KeyFactory.getInstance("Ed25519")
|
|
145
|
+
val publicKey = keyFactory.generatePublic(keySpec)
|
|
146
|
+
val signature = Signature.getInstance("Ed25519")
|
|
147
|
+
signature.initVerify(publicKey)
|
|
148
|
+
signature.update(Base64.decode(messageBase64, Base64.NO_WRAP))
|
|
149
|
+
promise.resolve(signature.verify(Base64.decode(signatureBase64, Base64.NO_WRAP)))
|
|
150
|
+
} catch (error: Exception) {
|
|
151
|
+
promise.reject("verify_ed25519_failed", error)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
override fun verifyHmacSha256(keyId: String, messageBase64: String, macBase64: String, promise: Promise) {
|
|
156
|
+
try {
|
|
157
|
+
val keyMaterial = resolveKeyMaterial(keyId)
|
|
158
|
+
if (keyMaterial == null) {
|
|
159
|
+
promise.resolve(false)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
val mac = Mac.getInstance("HmacSHA256")
|
|
163
|
+
mac.init(SecretKeySpec(keyMaterial, "HmacSHA256"))
|
|
164
|
+
val computed = mac.doFinal(Base64.decode(messageBase64, Base64.NO_WRAP))
|
|
165
|
+
val expected = Base64.decode(macBase64, Base64.NO_WRAP)
|
|
166
|
+
promise.resolve(constantTimeEquals(computed, expected))
|
|
167
|
+
} catch (error: Exception) {
|
|
168
|
+
promise.reject("verify_hmac_sha256_failed", error)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private fun constantTimeEquals(a: ByteArray, b: ByteArray): Boolean {
|
|
173
|
+
if (a.size != b.size) return false
|
|
174
|
+
var diff = 0
|
|
175
|
+
for (i in a.indices) diff = diff or (a[i].toInt() xor b[i].toInt())
|
|
176
|
+
return diff == 0
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// --- Background scheduling (doc 01 §4.1 "scheduler" row) ---
|
|
180
|
+
//
|
|
181
|
+
// WorkManager PeriodicWorkRequest, minimum periodic interval is 15 minutes (an
|
|
182
|
+
// OS-enforced floor on Android, not a choice of this library); requests below that
|
|
183
|
+
// are rounded up. VersionCheckWorker wakes VersionCheckHeadlessTaskService, which
|
|
184
|
+
// runs the "VersionCheckBackgroundTask" headless JS task the host app must register
|
|
185
|
+
// (see registerVersionCheckHeadlessTask() in the JS public API).
|
|
186
|
+
|
|
187
|
+
override fun scheduleBackgroundCheck(taskId: String, minIntervalMs: Double, promise: Promise) {
|
|
188
|
+
try {
|
|
189
|
+
val intervalMinutes = maxOf(15L, (minIntervalMs / 60_000.0).toLong())
|
|
190
|
+
val request = PeriodicWorkRequestBuilder<VersionCheckWorker>(intervalMinutes, TimeUnit.MINUTES)
|
|
191
|
+
.addTag(taskId)
|
|
192
|
+
.build()
|
|
193
|
+
WorkManager.getInstance(reactApplicationContext)
|
|
194
|
+
.enqueueUniquePeriodicWork(taskId, ExistingPeriodicWorkPolicy.UPDATE, request)
|
|
195
|
+
promise.resolve(null)
|
|
196
|
+
} catch (error: Exception) {
|
|
197
|
+
promise.reject("schedule_background_check_failed", error)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
override fun cancelBackgroundCheck(taskId: String, promise: Promise) {
|
|
202
|
+
try {
|
|
203
|
+
WorkManager.getInstance(reactApplicationContext).cancelUniqueWork(taskId)
|
|
204
|
+
promise.resolve(null)
|
|
205
|
+
} catch (error: Exception) {
|
|
206
|
+
promise.reject("cancel_background_check_failed", error)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
companion object {
|
|
211
|
+
const val NAME = NativeVersionCheckSpec.NAME
|
|
212
|
+
private const val PREFS_NAME = "rs_version_check_prefs"
|
|
213
|
+
private const val SECURE_PREFS_NAME = "rs_version_check_secure_prefs"
|
|
214
|
+
}
|
|
215
|
+
}
|