@@ -9,6 +9,54 @@ const PackageMixins = require("./package-mixins")(NativeCodePush);
99
1010const DEPLOYMENT_KEY = 'deprecated_deployment_key' ;
1111
12+ /**
13+ * @param deviceId {string}
14+ * @returns {number }
15+ */
16+ function hashDeviceId ( deviceId ) {
17+ let hash = 0 ;
18+ for ( let i = 0 ; i < deviceId . length ; i ++ ) {
19+ hash = ( ( hash << 5 ) - hash ) + deviceId . charCodeAt ( i ) ;
20+ hash |= 0 ; // Convert to 32bit int
21+ }
22+ return Math . abs ( hash ) ;
23+ }
24+
25+ /**
26+ * @param clientId {string}
27+ * @param packageHash {string}
28+ * @returns {number }
29+ */
30+ function getBucket ( clientId , packageHash ) {
31+ const hash = hashDeviceId ( `${ clientId ?? '' } _${ packageHash ?? '' } ` ) ;
32+ return ( Math . abs ( hash ) % 100 ) ;
33+ }
34+
35+ /**
36+ * Note that the `clientUniqueId` value may not guarantee the same value if the app is deleted and re-installed.
37+ * In other words, if a user re-installs the app, the result of this function may change.
38+ * @returns {Promise<boolean> }
39+ */
40+ async function decideLatestReleaseIsInRollout ( versioning , clientId , onRolloutSkipped ) {
41+ const [ latestVersion , latestReleaseInfo ] = versioning . findLatestRelease ( ) ;
42+
43+ if ( latestReleaseInfo . rollout === undefined || latestReleaseInfo . rollout >= 100 ) {
44+ return true ;
45+ }
46+
47+ const bucket = getBucket ( clientId , latestReleaseInfo . packageHash ) ;
48+ const inRollout = bucket < latestReleaseInfo . rollout ;
49+
50+ log ( `Bucket: ${ bucket } , rollout: ${ latestReleaseInfo . rollout } → ${ inRollout ? 'IN' : 'OUT' } ` ) ;
51+
52+ if ( ! inRollout ) {
53+ log ( `Skipping update due to rollout. Bucket ${ bucket } is not smaller than rollout range ${ latestReleaseInfo . rollout } .` ) ;
54+ onRolloutSkipped ?. ( latestVersion ) ;
55+ }
56+
57+ return inRollout ;
58+ }
59+
1260async function checkForUpdate ( handleBinaryVersionMismatchCallback = null ) {
1361 /*
1462 * Before we ask the server if an update exists, we
@@ -42,6 +90,9 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
4290 }
4391 }
4492
93+ /**
94+ * @type {RemotePackage|null|undefined }
95+ */
4596 const update = await ( async ( ) => {
4697 try {
4798 const updateRequest = {
@@ -58,8 +109,8 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
58109 */
59110 const updateChecker = sharedCodePushOptions . updateChecker ;
60111 if ( updateChecker ) {
112+ // We do not provide rollout functionality. This could be implemented in the `updateChecker`.
61113 const { update_info } = await updateChecker ( updateRequest ) ;
62-
63114 return mapToRemotePackageMetadata ( update_info ) ;
64115 } else {
65116 /**
@@ -77,6 +128,9 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
77128
78129 const versioning = new SemverVersioning ( releaseHistory ) ;
79130
131+ const isInRollout = await decideLatestReleaseIsInRollout ( versioning , nativeConfig . clientUniqueId , sharedCodePushOptions ?. onRolloutSkipped ) ;
132+ versioning . setIsLatestReleaseInRollout ( isInRollout ) ;
133+
80134 const shouldRollbackToBinary = versioning . shouldRollbackToBinary ( runtimeVersion )
81135 if ( shouldRollbackToBinary ) {
82136 // Reset to latest major version and restart
@@ -217,7 +271,7 @@ async function getCurrentPackage() {
217271async function getUpdateMetadata ( updateState ) {
218272 let updateMetadata = await NativeCodePush . getUpdateMetadata ( updateState || CodePush . UpdateState . RUNNING ) ;
219273 if ( updateMetadata ) {
220- updateMetadata = { ...PackageMixins . local , ...updateMetadata } ;
274+ updateMetadata = { ...PackageMixins . local , ...updateMetadata } ;
221275 updateMetadata . failedInstall = await NativeCodePush . isFailedUpdate ( updateMetadata . packageHash ) ;
222276 updateMetadata . isFirstRun = await NativeCodePush . isFirstRun ( updateMetadata . packageHash ) ;
223277 }
@@ -424,47 +478,47 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
424478 mandatoryInstallMode : CodePush . InstallMode . IMMEDIATE ,
425479 minimumBackgroundDuration : 0 ,
426480 updateDialog : null ,
427- ...options
481+ ...options ,
428482 } ;
429483
430484 syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
431485 ? syncStatusChangeCallback
432486 : ( syncStatus ) => {
433- switch ( syncStatus ) {
434- case CodePush . SyncStatus . CHECKING_FOR_UPDATE :
435- log ( "Checking for update." ) ;
436- break ;
437- case CodePush . SyncStatus . AWAITING_USER_ACTION :
438- log ( "Awaiting user action." ) ;
439- break ;
440- case CodePush . SyncStatus . DOWNLOADING_PACKAGE :
441- log ( "Downloading package." ) ;
442- break ;
443- case CodePush . SyncStatus . INSTALLING_UPDATE :
444- log ( "Installing update." ) ;
445- break ;
446- case CodePush . SyncStatus . UP_TO_DATE :
447- log ( "App is up to date." ) ;
448- break ;
449- case CodePush . SyncStatus . UPDATE_IGNORED :
450- log ( "User cancelled the update." ) ;
451- break ;
452- case CodePush . SyncStatus . UPDATE_INSTALLED :
453- if ( resolvedInstallMode == CodePush . InstallMode . ON_NEXT_RESTART ) {
454- log ( "Update is installed and will be run on the next app restart." ) ;
455- } else if ( resolvedInstallMode == CodePush . InstallMode . ON_NEXT_RESUME ) {
456- if ( syncOptions . minimumBackgroundDuration > 0 ) {
457- log ( `Update is installed and will be run after the app has been in the background for at least ${ syncOptions . minimumBackgroundDuration } seconds.` ) ;
458- } else {
459- log ( "Update is installed and will be run when the app next resumes." ) ;
460- }
487+ switch ( syncStatus ) {
488+ case CodePush . SyncStatus . CHECKING_FOR_UPDATE :
489+ log ( "Checking for update." ) ;
490+ break ;
491+ case CodePush . SyncStatus . AWAITING_USER_ACTION :
492+ log ( "Awaiting user action." ) ;
493+ break ;
494+ case CodePush . SyncStatus . DOWNLOADING_PACKAGE :
495+ log ( "Downloading package." ) ;
496+ break ;
497+ case CodePush . SyncStatus . INSTALLING_UPDATE :
498+ log ( "Installing update." ) ;
499+ break ;
500+ case CodePush . SyncStatus . UP_TO_DATE :
501+ log ( "App is up to date." ) ;
502+ break ;
503+ case CodePush . SyncStatus . UPDATE_IGNORED :
504+ log ( "User cancelled the update." ) ;
505+ break ;
506+ case CodePush . SyncStatus . UPDATE_INSTALLED :
507+ if ( resolvedInstallMode == CodePush . InstallMode . ON_NEXT_RESTART ) {
508+ log ( "Update is installed and will be run on the next app restart." ) ;
509+ } else if ( resolvedInstallMode == CodePush . InstallMode . ON_NEXT_RESUME ) {
510+ if ( syncOptions . minimumBackgroundDuration > 0 ) {
511+ log ( `Update is installed and will be run after the app has been in the background for at least ${ syncOptions . minimumBackgroundDuration } seconds.` ) ;
512+ } else {
513+ log ( "Update is installed and will be run when the app next resumes." ) ;
461514 }
462- break ;
463- case CodePush . SyncStatus . UNKNOWN_ERROR :
464- log ( "An unknown error occurred." ) ;
465- break ;
466- }
467- } ;
515+ }
516+ break ;
517+ case CodePush . SyncStatus . UNKNOWN_ERROR :
518+ log ( "An unknown error occurred." ) ;
519+ break ;
520+ }
521+ } ;
468522
469523 let remotePackageLabel ;
470524 try {
@@ -497,7 +551,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
497551
498552 if ( ! remotePackage || updateShouldBeIgnored ) {
499553 if ( updateShouldBeIgnored ) {
500- log ( "An update is available, but it is being ignored due to having been previously rolled back." ) ;
554+ log ( "An update is available, but it is being ignored due to having been previously rolled back." ) ;
501555 }
502556
503557 const currentPackage = await CodePush . getCurrentPackage ( ) ;
@@ -536,18 +590,18 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
536590 onPress : ( ) => {
537591 syncStatusChangeCallback ( CodePush . SyncStatus . UPDATE_IGNORED ) ;
538592 resolve ( CodePush . SyncStatus . UPDATE_IGNORED ) ;
539- }
593+ } ,
540594 } ) ;
541595 }
542596
543597 // Since the install button should be placed to the
544598 // right of any other button, add it last
545599 dialogButtons . push ( {
546600 text : installButtonText ,
547- onPress :( ) => {
601+ onPress : ( ) => {
548602 doDownloadAndInstall ( )
549603 . then ( resolve , reject ) ;
550- }
604+ } ,
551605 } )
552606
553607 // If the update has a description, and the developer
@@ -609,6 +663,9 @@ let CodePush;
609663 *
610664 * onSyncError: (label: string, error: Error) => void | undefined,
611665 * setOnSyncError(onSyncErrorFunction: (label: string, error: Error) => void | undefined): void,
666+ *
667+ * onRolloutSkipped: (label: string, error: Error) => void | undefined,
668+ * setOnRolloutSkipped(onRolloutSkippedFunction: (label: string, error: Error) => void | undefined): void,
612669 * }}
613670 */
614671const sharedCodePushOptions = {
@@ -653,6 +710,12 @@ const sharedCodePushOptions = {
653710 if ( typeof onSyncErrorFunction !== 'function' ) throw new Error ( 'Please pass a function to onSyncError' ) ;
654711 this . onSyncError = onSyncErrorFunction ;
655712 } ,
713+ onRolloutSkipped : undefined ,
714+ setOnRolloutSkipped ( onRolloutSkippedFunction ) {
715+ if ( ! onRolloutSkippedFunction ) return ;
716+ if ( typeof onRolloutSkippedFunction !== 'function' ) throw new Error ( 'Please pass a function to onRolloutSkipped' ) ;
717+ this . onRolloutSkipped = onRolloutSkippedFunction ;
718+ } ,
656719}
657720
658721function codePushify ( options = { } ) {
@@ -671,7 +734,7 @@ function codePushify(options = {}) {
671734 throw new Error (
672735`Unable to find the "Component" class, please either:
6737361. Upgrade to a newer version of React Native that supports it, or
674- 2. Call the codePush.sync API in your component instead of using the @codePush decorator`
737+ 2. Call the codePush.sync API in your component instead of using the @codePush decorator` ,
675738 ) ;
676739 }
677740
@@ -688,6 +751,7 @@ function codePushify(options = {}) {
688751 sharedCodePushOptions . setOnDownloadStart ( options . onDownloadStart ) ;
689752 sharedCodePushOptions . setOnDownloadSuccess ( options . onDownloadSuccess ) ;
690753 sharedCodePushOptions . setOnSyncError ( options . onSyncError ) ;
754+ sharedCodePushOptions . setOnRolloutSkipped ( options . onRolloutSkipped ) ;
691755
692756 const decorator = ( RootComponent ) => {
693757 class CodePushComponent extends React . Component {
@@ -730,7 +794,7 @@ function codePushify(options = {}) {
730794 }
731795
732796 render ( ) {
733- const props = { ...this . props } ;
797+ const props = { ...this . props } ;
734798
735799 // We can set ref property on class components only (not stateless)
736800 // Check it by render method
@@ -777,7 +841,7 @@ if (NativeCodePush) {
777841 IMMEDIATE : NativeCodePush . codePushInstallModeImmediate , // Restart the app immediately
778842 ON_NEXT_RESTART : NativeCodePush . codePushInstallModeOnNextRestart , // Don't artificially restart the app. Allow the update to be "picked up" on the next app restart
779843 ON_NEXT_RESUME : NativeCodePush . codePushInstallModeOnNextResume , // Restart the app the next time it is resumed from the background
780- ON_NEXT_SUSPEND : NativeCodePush . codePushInstallModeOnNextSuspend // Restart the app _while_ it is in the background,
844+ ON_NEXT_SUSPEND : NativeCodePush . codePushInstallModeOnNextSuspend , // Restart the app _while_ it is in the background,
781845 // but only after it has been in the background for "minimumBackgroundDuration" seconds (0 by default),
782846 // so that user context isn't lost unless the app suspension is long enough to not matter
783847 } ,
@@ -790,17 +854,17 @@ if (NativeCodePush) {
790854 CHECKING_FOR_UPDATE : 5 ,
791855 AWAITING_USER_ACTION : 6 ,
792856 DOWNLOADING_PACKAGE : 7 ,
793- INSTALLING_UPDATE : 8
857+ INSTALLING_UPDATE : 8 ,
794858 } ,
795859 CheckFrequency : {
796860 ON_APP_START : 0 ,
797861 ON_APP_RESUME : 1 ,
798- MANUAL : 2
862+ MANUAL : 2 ,
799863 } ,
800864 UpdateState : {
801865 RUNNING : NativeCodePush . codePushUpdateStateRunning ,
802866 PENDING : NativeCodePush . codePushUpdateStatePending ,
803- LATEST : NativeCodePush . codePushUpdateStateLatest
867+ LATEST : NativeCodePush . codePushUpdateStateLatest ,
804868 } ,
805869 DeploymentStatus : {
806870 FAILED : "DeploymentFailed" ,
@@ -814,11 +878,11 @@ if (NativeCodePush) {
814878 optionalIgnoreButtonLabel : "Ignore" ,
815879 optionalInstallButtonLabel : "Install" ,
816880 optionalUpdateMessage : "An update is available. Would you like to install it?" ,
817- title : "Update available"
881+ title : "Update available" ,
818882 } ,
819883 DEFAULT_ROLLBACK_RETRY_OPTIONS : {
820884 delayInHours : 24 ,
821- maxRetryAttempts : 1
885+ maxRetryAttempts : 1 ,
822886 } ,
823887 } ) ;
824888} else {
0 commit comments