Skip to content
This repository has been archived by the owner on Jan 18, 2024. It is now read-only.

Commit

Permalink
[config] allow scheme arrays (#2462)
Browse files Browse the repository at this point in the history
* Allow for array of schemes and platform specific schemes to be added to prod builds

* Added bundleId and package name to scheme list

* Normalize schemes array for turtle
  • Loading branch information
EvanBacon authored Aug 17, 2020
1 parent ad61fa9 commit e7bf70a
Show file tree
Hide file tree
Showing 10 changed files with 150 additions and 46 deletions.
16 changes: 15 additions & 1 deletion packages/config/src/Config.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,13 @@ type AndroidAdaptiveIcon = {
};

export type AndroidPlatformConfig = {
/**
* URL scheme to link into your app. For example, if we set this to `'demo'`, then demo:// URLs would open your app when tapped.
* @pattern ^[a-z][a-z0-9+.-]*$
* @regexHuman String beginning with a lowercase letter followed by any combination of lowercase letters, digits, \"+\", \".\" or \"-\"
* @standaloneOnly
*/
scheme?: string | string[];
/**
* @autogenerated
*/
Expand Down Expand Up @@ -613,6 +620,13 @@ export type WebSplashScreen = {
};

export type IosPlatformConfig = {
/**
* URL scheme to link into your app. For example, if we set this to `'demo'`, then demo:// URLs would open your app when tapped.
* @pattern ^[a-z][a-z0-9+.-]*$
* @regexHuman String beginning with a lowercase letter followed by any combination of lowercase letters, digits, \"+\", \".\" or \"-\"
* @standaloneOnly
*/
scheme?: string | string[];
/**
* @autogenerated
*/
Expand Down Expand Up @@ -899,7 +913,7 @@ export type ExpoConfig = {
* @regexHuman String beginning with a lowercase letter followed by any combination of lowercase letters, digits, \"+\", \".\" or \"-\"
* @standaloneOnly
*/
scheme?: string;
scheme?: string | string[];
/**
* The relative path to your main JavaScript file.
*/
Expand Down
28 changes: 21 additions & 7 deletions packages/config/src/android/Scheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,41 @@ export type IntentFilterProps = {
schemes: string[];
};

export function getScheme(config: ExpoConfig) {
return typeof config.scheme === 'string' ? config.scheme : null;
export function getScheme(config: { scheme?: string | string[] }): string[] {
if (Array.isArray(config.scheme)) {
function validate(value: any): value is string {
return typeof value === 'string';
}
return config.scheme.filter<string>(validate);
} else if (typeof config.scheme === 'string') {
return [config.scheme];
}
return [];
}

export async function setScheme(config: ExpoConfig, manifestDocument: Document) {
const scheme = getScheme(config);
if (!scheme) {
export async function setScheme(
config: Pick<ExpoConfig, 'scheme' | 'android'>,
manifestDocument: Document
) {
const scheme = [...getScheme(config), ...getScheme(config.android ?? {})];
// Add the package name to the list of schemes for easier Google auth and parity with Turtle v1.
if (config.android?.['package']) {
scheme.push(config.android['package']);
}
if (scheme.length === 0) {
return manifestDocument;
}

const mainActivity = manifestDocument.manifest.application[0].activity.filter(
(e: any) => e['$']['android:name'] === '.MainActivity'
);

const schemeTag = `<data android:scheme="${scheme}"/>`;
const intentFiltersXML = `
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
${schemeTag}
${scheme.map(scheme => `<data android:scheme="${scheme}"/>`).join('\n')}
</intent-filter>`;
const parser = new Parser();
const intentFiltersJSON = await parser.parseStringPromise(intentFiltersXML);
Expand Down
41 changes: 31 additions & 10 deletions packages/config/src/android/__tests__/Scheme-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,18 @@ const fixturesPath = resolve(__dirname, 'fixtures');
const sampleManifestPath = resolve(fixturesPath, 'react-native-AndroidManifest.xml');

describe('scheme', () => {
it(`returns null if no scheme is provided`, () => {
expect(getScheme({})).toBe(null);
it(`returns empty array if no scheme is provided`, () => {
expect(getScheme({})).toStrictEqual([]);
});

it(`returns the scheme if provided`, () => {
expect(getScheme({ scheme: 'myapp' })).toBe('myapp');
expect(getScheme({ scheme: 'myapp' })).toStrictEqual(['myapp']);
expect(getScheme({ scheme: ['other', 'myapp'] })).toStrictEqual(['other', 'myapp']);
expect(
getScheme({
scheme: ['other', 'myapp', null],
})
).toStrictEqual(['other', 'myapp']);
});

it('does not add scheme if none provided', async () => {
Expand All @@ -32,18 +38,33 @@ describe('scheme', () => {

it('adds scheme to android manifest', async () => {
let androidManifestJson = await readAndroidManifestAsync(sampleManifestPath);
androidManifestJson = await setScheme({ scheme: 'myapp' }, androidManifestJson);
androidManifestJson = await setScheme(
{
scheme: 'myapp',
android: { scheme: ['android-only'], package: 'com.demo.value' },
ios: { scheme: 'ios-only' },
},
androidManifestJson
);

const intentFilters = androidManifestJson.manifest.application[0].activity.filter(
e => e['$']['android:name'] === '.MainActivity'
)[0]['intent-filter'];
const schemeIntent = intentFilters.filter(e => {
if (e.hasOwnProperty('data')) {
return e['data'][0]['$']['android:scheme'] === 'myapp';

const schemeIntent = [];

for (const intent of intentFilters) {
if ('data' in intent) {
for (const dataFilter of intent['data']) {
const possibleScheme = dataFilter['$']['android:scheme'];
if (possibleScheme) {
schemeIntent.push(possibleScheme);
}
}
}
return false;
});
expect(schemeIntent).toHaveLength(1);
}

expect(schemeIntent).toStrictEqual(['myapp', 'android-only', 'com.demo.value']);
});
});

Expand Down
27 changes: 21 additions & 6 deletions packages/config/src/ios/Scheme.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
import { ExpoConfig } from '../Config.types';
import { InfoPlist, URLScheme } from './IosConfig.types';

export function getScheme(config: Pick<ExpoConfig, 'scheme'>): string | null {
return typeof config.scheme === 'string' ? config.scheme : null;
export function getScheme(config: { scheme?: string | string[] }): string[] {
if (Array.isArray(config.scheme)) {
function validate(value: any): value is string {
return typeof value === 'string';
}
return config.scheme.filter<string>(validate);
} else if (typeof config.scheme === 'string') {
return [config.scheme];
}
return [];
}

export function setScheme(config: Pick<ExpoConfig, 'scheme'>, infoPlist: InfoPlist): InfoPlist {
const scheme = getScheme(config);
if (!scheme) {
export function setScheme(
config: Pick<ExpoConfig, 'scheme' | 'ios'>,
infoPlist: InfoPlist
): InfoPlist {
const scheme = [...getScheme(config), ...getScheme(config.ios ?? {})];
// Add the bundle identifier to the list of schemes for easier Google auth and parity with Turtle v1.
if (config.ios?.bundleIdentifier) {
scheme.push(config.ios.bundleIdentifier);
}
if (scheme.length === 0) {
return infoPlist;
}

return {
...infoPlist,
CFBundleURLTypes: [{ CFBundleURLSchemes: [scheme] }],
CFBundleURLTypes: [{ CFBundleURLSchemes: scheme }],
};
}

Expand Down
23 changes: 18 additions & 5 deletions packages/config/src/ios/__tests__/Scheme-test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import { getScheme, getSchemesFromPlist, hasScheme, removeScheme, setScheme } from '../Scheme';

describe('scheme', () => {
it(`returns null if no scheme is provided`, () => {
expect(getScheme({})).toBe(null);
it(`returns empty array if no scheme is provided`, () => {
expect(getScheme({})).toStrictEqual([]);
});

it(`returns the scheme if provided`, () => {
expect(getScheme({ scheme: 'myapp' })).toBe('myapp');
expect(getScheme({ scheme: 'myapp' })).toStrictEqual(['myapp']);
expect(
getScheme({
scheme: ['myapp', 'other', null],
})
).toStrictEqual(['myapp', 'other']);
});

it(`sets the CFBundleUrlTypes if scheme is given`, () => {
expect(setScheme({ scheme: 'myapp' }, {})).toMatchObject({
CFBundleURLTypes: [{ CFBundleURLSchemes: ['myapp'] }],
expect(
setScheme(
{
scheme: ['myapp', 'more'],
ios: { scheme: ['ios-only'], bundleIdentifier: 'com.demo.value' },
},
{}
)
).toMatchObject({
CFBundleURLTypes: [{ CFBundleURLSchemes: ['myapp', 'more', 'ios-only', 'com.demo.value'] }],
});
});

Expand Down
14 changes: 7 additions & 7 deletions packages/dev-server/src/MetroDevServer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import http from 'http';
import { Platform, getConfig, projectHasModule } from '@expo/config';
import { createDevServerMiddleware } from '@react-native-community/cli-server-api';
import Log from '@expo/bunyan';
import { Platform, getConfig, projectHasModule } from '@expo/config';
import * as ExpoMetroConfig from '@expo/metro-config';
import { createDevServerMiddleware } from '@react-native-community/cli-server-api';
import bodyParser from 'body-parser';
import http from 'http';
import type Metro from 'metro';

import LogReporter from './LogReporter';
Expand All @@ -26,7 +26,7 @@ export type BundleAssetWithFileHashes = Metro.AssetData & {
export type BundleOutput = {
code: string;
map: string;
assets: ReadonlyArray<BundleAssetWithFileHashes>;
assets: readonly BundleAssetWithFileHashes[];
};

export async function runMetroDevServerAsync(
Expand Down Expand Up @@ -118,9 +118,9 @@ export async function bundleAsync(
},
});
const { code, map } = await metroServer.build(bundleOptions);
const assets = (await metroServer.getAssets(bundleOptions)) as ReadonlyArray<
BundleAssetWithFileHashes
>;
const assets = (await metroServer.getAssets(
bundleOptions
)) as readonly BundleAssetWithFileHashes[];
reporter.update({
buildID,
type: 'bundle_build_done',
Expand Down
10 changes: 8 additions & 2 deletions packages/xdl/src/UrlUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,14 @@ export async function constructUrlAsync(

const { exp } = getConfig(projectRoot);
if (exp.detach) {
if (exp.scheme && Versions.gteSdkVersion(exp, '27.0.0')) {
protocol = exp.scheme;
// Normalize schemes and filter invalid schemes.
const schemes = (Array.isArray(exp.scheme) ? exp.scheme : [exp.scheme]).filter(
scheme => typeof scheme === 'string' && !!scheme
);
// Get the first valid scheme.
const firstScheme = schemes[0];
if (firstScheme && Versions.gteSdkVersion(exp, '27.0.0')) {
protocol = firstScheme;
} else if (exp.detach.scheme) {
// must keep this fallback in place for older projects
// and those detached with an older version of xdl
Expand Down
14 changes: 12 additions & 2 deletions packages/xdl/src/detach/AndroidShellApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,14 @@ function getPrivateConfig(context) {
}
}

function ensureStringArray(input) {
// Normalize schemes and filter invalid schemes.
const stringArray = (Array.isArray(input) ? input : [input]).filter(
scheme => typeof scheme === 'string' && !!scheme
);
return stringArray;
}

export async function runShellAppModificationsAsync(context, sdkVersion, buildMode) {
const fnLogger = logger.withFields({ buildPhase: 'running shell app modifications' });

Expand Down Expand Up @@ -396,7 +404,10 @@ export async function runShellAppModificationsAsync(context, sdkVersion, buildMo
}

const name = manifest.name;
const scheme = manifest.scheme || (manifest.detach && manifest.detach.scheme);
const multiPlatformSchemes = ensureStringArray(manifest.scheme || manifest.detach?.scheme);
const androidSchemes = ensureStringArray(manifest.android?.scheme);
const schemes = [...multiPlatformSchemes, ...androidSchemes];
const scheme = schemes[0];
const bundleUrl = manifest.bundleUrl;
const isFullManifest = !!bundleUrl;
const version = manifest.version ? manifest.version : '0.0.0';
Expand Down Expand Up @@ -726,7 +737,6 @@ export async function runShellAppModificationsAsync(context, sdkVersion, buildMo
}

// Add shell app scheme
const schemes = [scheme].filter(e => e);
if (schemes.length > 0) {
const searchLine = isDetached
? '<!-- ADD DETACH SCHEME HERE -->'
Expand Down
8 changes: 5 additions & 3 deletions packages/xdl/src/detach/Detach.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,13 @@ async function _detachAsync(projectRoot, options) {
exp.detach.scheme = generatedScheme;
}

const linkingWarning = `You have not specified a custom scheme for deep linking. A default value of ${generatedScheme} will be used. You can change this later by following the instructions in this guide: https://docs.expo.io/workflow/linking/`;
if (!exp.scheme) {
logger.info(
`You have not specified a custom scheme for deep linking. A default value of ${generatedScheme} will be used. You can change this later by following the instructions in this guide: https://docs.expo.io/workflow/linking/`
);
logger.info(linkingWarning);
exp.scheme = generatedScheme;
} else if (Array.isArray(exp.scheme) && exp.scheme.length === 0) {
logger.info(linkingWarning);
exp.scheme.push(generatedScheme);
}

const expoDirectory = path.join(projectRoot, '.expo-source');
Expand Down
15 changes: 12 additions & 3 deletions packages/xdl/src/detach/IosNSBundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,14 @@ function _isAppleUsageDescriptionKey(key: string): boolean {
return key.includes('UsageDescription');
}

function ensureStringArray(input: any): string[] {
// Normalize schemes and filter invalid schemes.
const stringArray = (Array.isArray(input) ? input : [input]).filter(
scheme => typeof scheme === 'string' && !!scheme
);
return stringArray;
}

/**
* Configure an iOS Info.plist for a standalone app.
*/
Expand Down Expand Up @@ -280,8 +288,7 @@ async function _configureInfoPlistAsync(context: AnyStandaloneContext): Promise<
}

// bundle id
infoPlist.CFBundleIdentifier =
config.ios && config.ios.bundleIdentifier ? config.ios.bundleIdentifier : null;
infoPlist.CFBundleIdentifier = config?.ios?.bundleIdentifier ?? null;
if (!infoPlist.CFBundleIdentifier) {
throw new Error(`Cannot configure an iOS app with no bundle identifier.`);
}
Expand All @@ -293,7 +300,9 @@ async function _configureInfoPlistAsync(context: AnyStandaloneContext): Promise<
: config.name;

// determine app linking schemes
const linkingSchemes = config.scheme ? [config.scheme] : [];
const multiPlatformSchemes = ensureStringArray(config.scheme);
const iosSchemes = ensureStringArray(config.ios?.scheme);
const linkingSchemes = [...multiPlatformSchemes, ...iosSchemes];
if (config.facebookScheme && config.facebookScheme.startsWith('fb')) {
linkingSchemes.push(config.facebookScheme);
}
Expand Down

0 comments on commit e7bf70a

Please sign in to comment.