Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public static CodePush getInstance(String deploymentKey, Context context, boolea
// Config properties.
private String mDeploymentKey;
private static String mServerUrl = "https://codepush.appcenter.ms/";
private final boolean mEnableDeltaUpdates;

private Context mContext;
private final boolean mIsDebugMode;
Expand All @@ -70,8 +71,8 @@ public static String getServiceUrl() {
private CodePush(String deploymentKey, Context context, boolean isDebugMode) {
mContext = context.getApplicationContext();

boolean enableDeltaUpdates = getBooleanCustomPropertyFromStringsIfExist("EnableDeltaUpdates", false);
mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath(), enableDeltaUpdates);
mEnableDeltaUpdates = getBooleanCustomPropertyFromStringsIfExist("EnableDeltaUpdates", false);
mUpdateManager = new CodePushUpdateManager(context.getFilesDir().getAbsolutePath(), mEnableDeltaUpdates);
mTelemetryManager = new CodePushTelemetryManager(mContext);
mDeploymentKey = deploymentKey;
mIsDebugMode = isDebugMode;
Expand Down Expand Up @@ -289,6 +290,10 @@ public String getServerUrl() {
return mServerUrl;
}

public boolean isDeltaUpdatesEnabled() {
return mEnableDeltaUpdates;
}

void initializeUpdateAfterRestart() {
// Reset the state which indicates that
// the app was just freshly updated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ public void getConfiguration(Promise promise) {
configMap.putString("clientUniqueId", mClientUniqueId);
configMap.putString("deploymentKey", mCodePush.getDeploymentKey());
configMap.putString("serverUrl", mCodePush.getServerUrl());
configMap.putBoolean("enableDeltaUpdates", mCodePush.isDeltaUpdatesEnabled());

// The binary hash may be null in debug builds
if (mBinaryContentsHash != null) {
Expand Down
16 changes: 16 additions & 0 deletions expo.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ const withCodePushInfoPlist = (config, options = {}) => {
if (options.ios && options.ios.CodePushPublicKey) {
config.modResults.CodePushPublicKey = options.ios.CodePushPublicKey;
}
if (typeof options.ios?.CodePushEnableDeltaUpdates === 'boolean') {
config.modResults.CodePushEnableDeltaUpdates = options.ios.CodePushEnableDeltaUpdates;
}
return config;
});
};
Expand Down Expand Up @@ -330,6 +333,19 @@ const withAndroidStrings = (config, options) => {
if (options.android?.CodePushPublicKey) {
setString('CodePushPublicKey', options.android.CodePushPublicKey);
}
// Read natively as a bool resource (not a string), so it goes into its own <bool> element.
if (typeof options.android?.CodePushEnableDeltaUpdates === 'boolean') {
if (!config.modResults.resources.bool) config.modResults.resources.bool = [];
const bools = config.modResults.resources.bool;
const name = 'CodePushEnableDeltaUpdates';
const value = String(options.android.CodePushEnableDeltaUpdates);
const existing = bools.find(b => b.$.name === name);
if (existing) {
existing._ = value;
} else {
bools.push({ $: { name }, _: value });
}
}
return config;
});
};
Expand Down
8 changes: 7 additions & 1 deletion ios/CodePush/CodePushConfig.m
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#import "CodePush.h"
#import <UIKit/UIKit.h>

// Legacy untyped store: do not add new fields here. Keep new config as typed properties
// (like enableDeltaUpdates) and serialize them only in -configuration, at the JS boundary.
// A TurboModule spec with codegen should eventually replace this dictionary entirely.
@implementation CodePushConfig {
NSMutableDictionary *_configDictionary;
}
Expand All @@ -13,6 +16,7 @@ @implementation CodePushConfig {
static NSString * const DeploymentKeyConfigKey = @"deploymentKey";
static NSString * const ServerURLConfigKey = @"serverUrl";
static NSString * const PublicKeyKey = @"publicKey";
static NSString * const EnableDeltaUpdatesConfigKey = @"enableDeltaUpdates";

+ (instancetype)current
{
Expand Down Expand Up @@ -74,7 +78,9 @@ - (NSString *)buildVersion

- (NSDictionary *)configuration
{
return _configDictionary;
NSMutableDictionary *configuration = [_configDictionary mutableCopy];
configuration[EnableDeltaUpdatesConfigKey] = @(_enableDeltaUpdates);
return configuration;
}

- (NSString *)deploymentKey
Expand Down
133 changes: 133 additions & 0 deletions src/acquisition-sdk/__tests__/capabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import * as assert from "assert";
import * as querystring from "querystring";

import * as acquisitionSdk from "../acquisition-sdk";
import * as mockApi from "./acquisition-rest-mock";

const configuration: acquisitionSdk.Configuration = {
appVersion: "1.5.0",
clientUniqueId: "My iPhone",
deploymentKey: mockApi.validDeploymentKey,
serverUrl: mockApi.serverUrl,
enableDeltaUpdates: true
};

const deltaUpdatesDisabledConfiguration: acquisitionSdk.Configuration = { ...configuration, enableDeltaUpdates: undefined };

const currentPackage: acquisitionSdk.Package = {
deploymentKey: mockApi.validDeploymentKey,
description: "Standard description",
label: "v1",
appVersion: "1.5.0",
packageHash: "hash001",
isMandatory: false,
packageSize: 100
};

const okResponse: acquisitionSdk.Http.Response = {
statusCode: 200,
body: JSON.stringify({ update_info: { is_available: false } })
};

// Captures the exact request the SDK sends, instead of simulating a server response - the
// point of these tests is what goes over the wire, not how the SDK reacts to a reply.
class CapturingRequester implements acquisitionSdk.Http.Requester {
public lastUrl: string;
public lastBody: string;

public request(
verb: acquisitionSdk.Http.Verb,
url: string,
requestBodyOrCallback: string | acquisitionSdk.Callback<acquisitionSdk.Http.Response>,
callback?: acquisitionSdk.Callback<acquisitionSdk.Http.Response>
): void {
this.lastUrl = url;

if (typeof requestBodyOrCallback === "string") {
this.lastBody = requestBodyOrCallback;
callback(/*error*/ null, okResponse);
} else {
requestBodyOrCallback(/*error*/ null, okResponse);
}
}
}

describe("Capabilities advertisement", () => {
it("update_check sends capabilities as a plain repeated query param, not bracketed", (done: Mocha.Done) => {
var requester = new CapturingRequester();
var acquisition = new acquisitionSdk.AcquisitionManager(requester, configuration);

acquisition.queryUpdateWithCurrentPackage(currentPackage, () => {
var query = requester.lastUrl.split("?")[1];
var params = querystring.parse(query);

assert.strictEqual(params.capabilities, "binary_diff:bsdiff");
assert.strictEqual(query.includes("capabilities%5B%5D"), false, "must not use bracket notation");
done();
});
});

it("update_check omits undefined optional fields instead of the literal string \"undefined\"", (done: Mocha.Done) => {
var requester = new CapturingRequester();
var acquisition = new acquisitionSdk.AcquisitionManager(requester, configuration);
var freshInstallPackage: acquisitionSdk.Package = { ...currentPackage, packageHash: undefined, label: undefined };

acquisition.queryUpdateWithCurrentPackage(freshInstallPackage, () => {
var query = requester.lastUrl.split("?")[1];
var params = querystring.parse(query);

assert.strictEqual(params.package_hash, undefined);
assert.strictEqual(params.label, undefined);
assert.strictEqual(query.includes("undefined"), false);
done();
});
});

it("report_status/deploy sends capabilities as a JSON array", (done: Mocha.Done) => {
var requester = new CapturingRequester();
var acquisition = new acquisitionSdk.AcquisitionManager(requester, configuration);

acquisition.reportStatusDeploy(
currentPackage,
acquisitionSdk.AcquisitionStatus.DeploymentSucceeded,
/*previousLabelOrAppVersion*/ undefined,
/*previousDeploymentKey*/ undefined,
() => {
var body = JSON.parse(requester.lastBody);

assert.deepStrictEqual(body.capabilities, ["binary_diff:bsdiff"]);
done();
}
);
});

it("update_check sends no capabilities when delta updates are not enabled", (done: Mocha.Done) => {
var requester = new CapturingRequester();
var acquisition = new acquisitionSdk.AcquisitionManager(requester, deltaUpdatesDisabledConfiguration);

acquisition.queryUpdateWithCurrentPackage(currentPackage, () => {
var params = querystring.parse(requester.lastUrl.split("?")[1]);

assert.strictEqual(params.capabilities, undefined);
done();
});
});

it("report_status/deploy sends an empty capabilities array when delta updates are not enabled", (done: Mocha.Done) => {
var requester = new CapturingRequester();
var acquisition = new acquisitionSdk.AcquisitionManager(requester, deltaUpdatesDisabledConfiguration);

acquisition.reportStatusDeploy(
currentPackage,
acquisitionSdk.AcquisitionStatus.DeploymentSucceeded,
/*previousLabelOrAppVersion*/ undefined,
/*previousDeploymentKey*/ undefined,
() => {
var body = JSON.parse(requester.lastBody);

assert.deepStrictEqual(body.capabilities, []);
done();
}
);
});
});
38 changes: 20 additions & 18 deletions src/acquisition-sdk/acquisition-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export interface Configuration {
deploymentKey: string;
serverUrl: string;
ignoreAppVersion?: boolean
enableDeltaUpdates?: boolean;
}

export class AcquisitionStatus {
Expand All @@ -72,6 +73,7 @@ export class DownloadStatus {
export class AcquisitionManager {
private readonly BASE_URL_PART = "appcenter.ms";
private _appVersion: string;
private _capabilities: string[];
private _clientUniqueId: string;
private _deploymentKey: string;
private _httpRequester: Http.Requester;
Expand All @@ -92,6 +94,10 @@ export class AcquisitionManager {
this._clientUniqueId = configuration.clientUniqueId;
this._deploymentKey = configuration.deploymentKey;
this._ignoreAppVersion = configuration.ignoreAppVersion;

// Sent on update_check and report_status/deploy. Must match what the native side will
// accept: it rejects binary diffs unless delta updates are enabled in the app config.
this._capabilities = configuration.enableDeltaUpdates ? ["binary_diff:bsdiff"] : [];
}

private isRecoverable = (statusCode: number): boolean => statusCode >= 500 || statusCode === 408 || statusCode === 429;
Expand Down Expand Up @@ -119,10 +125,11 @@ export class AcquisitionManager {
package_hash: currentPackage.packageHash,
is_companion: this._ignoreAppVersion,
label: currentPackage.label,
client_unique_id: this._clientUniqueId
client_unique_id: this._clientUniqueId,
capabilities: this._capabilities
};

var requestUrl: string = this._serverUrl + this._publicPrefixUrl + "update_check?" + queryStringify(updateRequest);
var requestUrl: string = this._serverUrl + this._publicPrefixUrl + "update_check?" + toQueryString(updateRequest);

this._httpRequester.request(Http.Verb.GET, requestUrl, (error: Error, response: Http.Response) => {
if (error) {
Expand Down Expand Up @@ -187,6 +194,7 @@ export class AcquisitionManager {
var url: string = this._serverUrl + this._publicPrefixUrl + "report_status/deploy";
var body: DeploymentStatusReport = {
app_version: this._appVersion,
capabilities: this._capabilities,
deployment_key: this._deploymentKey
};

Expand Down Expand Up @@ -283,25 +291,19 @@ export class AcquisitionManager {
}
}

function queryStringify(object: Object): string {
var queryString = "";
var isFirst: boolean = true;
// Built by hand because RN 0.76-0.79's URLSearchParams polyfill accepts only a plain object,
// which cannot hold a repeated key. RN 0.80 accepts [key, value] pairs, so this can become
// `new URLSearchParams(pairs)` once support for RN 0.76-0.79 is dropped.
function toQueryString(request: UpdateCheckRequest): string {
var pairs: string[] = [];

for (var property in object) {
if (object.hasOwnProperty(property)) {
var value: string = (<any>object)[property];
if (value !== null && typeof value !== "undefined") {
if (!isFirst) {
queryString += "&";
}

queryString += encodeURIComponent(property) + "=";
queryString += encodeURIComponent(value);
for (var [key, value] of Object.entries(request)) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decided to split the fix into its own PR: #64

for (var element of Array.isArray(value) ? value : [value]) {
if (element !== null && typeof element !== "undefined") {
pairs.push(encodeURIComponent(key) + "=" + encodeURIComponent(String(element)));
}

isFirst = false;
}
}

return queryString;
return pairs.join("&");
}
2 changes: 2 additions & 0 deletions src/acquisition-sdk/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/*in*/
export interface DeploymentStatusReport {
app_version: string;
capabilities?: string[];
client_unique_id?: string;
deployment_key: string;
previous_deployment_key?: string;
Expand Down Expand Up @@ -44,6 +45,7 @@ export interface UpdateCheckResponse {
/*in*/
export interface UpdateCheckRequest {
app_version: string;
capabilities?: string[];
client_unique_id?: string;
deployment_key: string;
is_companion?: boolean;
Expand Down
1 change: 1 addition & 0 deletions test/template/android/app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
<string name="app_name">TestCodePush</string>
<string moduleConfig="true" name="CodePushDeploymentKey">CODE_PUSH_ANDROID_DEPLOYMENT_KEY</string>
<string moduleConfig="true" name="CodePushServerUrl">CODE_PUSH_SERVER_URL</string>
<bool moduleConfig="true" name="CodePushEnableDeltaUpdates">true</bool>
</resources>
6 changes: 4 additions & 2 deletions test/template/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
"ios": {
"CodePushDeploymentKey": "mock-ios-deployment-key",
"CodePushServerURL": "http://127.0.0.1:3000",
"CodePushPublicKey": "{{CODE_SIGNING_PUBLIC_KEY}}"
"CodePushPublicKey": "{{CODE_SIGNING_PUBLIC_KEY}}",
"CodePushEnableDeltaUpdates": true
},
"android": {
"CodePushDeploymentKey": "mock-android-deployment-key",
"CodePushServerURL": "http://10.0.2.2:3001",
"CodePushPublicKey": "{{CODE_SIGNING_PUBLIC_KEY}}"
"CodePushPublicKey": "{{CODE_SIGNING_PUBLIC_KEY}}",
"CodePushEnableDeltaUpdates": true
}
}
]
Expand Down
7 changes: 7 additions & 0 deletions test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ async function setPlistStringValue(plistPath: string, key: string, value: string
await promisify(childProcess.execFile)("plutil", ["-replace", key, "-string", value, plistPath]);
}

async function setPlistBoolValue(plistPath: string, key: string, value: boolean): Promise<void> {
await promisify(childProcess.execFile)("plutil", ["-replace", key, "-bool", String(value), plistPath]);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plutil -replace inserts the key when it's missing, it doesn't require it to exist. The existing CodePushDeploymentKey, CodePushServerURL, and CodePushPublicKey lines work the same way. They are not in any template either.

}

/**
* Returns a " --platform <ios|android>" flag for `expo prebuild` when exactly one platform is
* under test in this mocha run, so prebuild only regenerates that platform's native project
Expand Down Expand Up @@ -253,6 +257,7 @@ class RNIOS extends Platform.IOS implements RNPlatform {
.then(() => setPlistStringValue(infoPlistPath, "CodePushDeploymentKey", this.getDefaultDeploymentKey()))
.then(() => setPlistStringValue(infoPlistPath, "CodePushServerURL", this.getServerUrl()))
.then(() => setPlistStringValue(infoPlistPath, "CodePushPublicKey", codeSigningPublicKey))
.then(() => setPlistBoolValue(infoPlistPath, "CodePushEnableDeltaUpdates", true))
// Fix the linker flag list in project.pbxproj (pod install adds an extra comma)
.then(TestUtil.replaceString.bind(undefined, path.join(iOSProject, TestConfig.TestAppName + ".xcodeproj", "project.pbxproj"),
"\"[$][(]inherited[)]\",\\s*[)];", "\"$(inherited)\"\n\t\t\t\t);"))
Expand Down Expand Up @@ -865,6 +870,8 @@ PluginTestingFramework.initializeTests(new RNProjectManager(), supportedTargetPl
try {
assert.notStrictEqual(null, request);
assert.strictEqual(request.query.deployment_key, targetPlatform.getDefaultDeploymentKey());
// The test apps enable delta updates, so this checks the flag end to end: native config -> getConfiguration() -> SDK.
assert.strictEqual(request.query.capabilities, "binary_diff:bsdiff");
} catch (e) {
done(e);
}
Expand Down
Loading