Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ Please visit [Twilio Docs](https://www.twilio.com/docs/flex/developer/plugins) f
* [Plugins API](https://www.twilio.com/docs/flex/developer/plugins/api)
* [Troubleshooting and FAQ](faq.md)

> **Note:** When using SSO v2 (OAuth flow), you may need to configure a custom domain for local development using the `--domain` flag to avoid authentication issues. See the [FAQ](faq.md) for setup instructions.

## Changelog

Major changelogs can be found in the [changelog directory](/changelog).
Expand Down
55 changes: 55 additions & 0 deletions faq.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,60 @@
# Troubleshooting & FAQ

#### Configuring Custom Domain for Local Development

You can configure a custom domain for local plugin development using the `--domain` flag. This is particularly important when using SSO v2 (OAuth flow), as `localhost` can cause authentication issues during login due to OAuth redirect URI restrictions.

**Why use a custom domain?**
- **SSO v2 OAuth Flow**: If you're using Twilio Flex with SSO v2 (OAuth authentication flow), `localhost` URLs may not be accepted as valid redirect URIs, causing login authentication failures
- **CORS Issues**: Some authentication systems restrict `localhost` for security reasons
- **Development Consistency**: Using a custom domain provides a more production-like development environment

**Usage:**
```bash
twilio flex:plugins:start --domain flex.local.com
```

**Setting up the custom domain:**
1. **macOS/Linux**: Add the domain to `/etc/hosts`
```bash
sudo echo "127.0.0.1 flex.local.com" >> /etc/hosts
```

2. **Windows**: Add the domain to `C:\Windows\System32\drivers\etc\hosts`
```
127.0.0.1 flex.local.com
```

**Available Options:**
- Without `--domain`: Uses `localhost` (default behavior, may cause issues with SSO v2)
- With `--domain flex.local.com`: Uses `flex.local.com`
- With `--domain my-custom.dev`: Uses any custom domain you prefer

**When is this required?**
- **SSO v2 Users**: If your Twilio Flex instance uses SSO v2 with OAuth authentication, you will likely encounter login failures when using `localhost`
- **CORS-restricted environments**: Some authentication providers block `localhost` requests
- **Custom OAuth configurations**: If your OAuth provider has strict redirect URI policies

After configuring your hosts file, your local plugin development server will be accessible at the specified domain (e.g., `http://flex.local.com:3000`).

#### SSO v2 OAuth Authentication Issues with localhost

If you're experiencing login failures during local development, especially with error messages related to OAuth redirects or authentication, this is likely due to SSO v2 OAuth flow restrictions with `localhost` URLs.

**Common Error Symptoms:**
- Login redirects fail or loop infinitely
- OAuth callback errors mentioning invalid redirect URI
- Authentication timeouts during local development
- CORS errors related to authentication endpoints

**Solution:**
Use the `--domain` flag with a custom domain:
```bash
twilio flex:plugins:start --domain flex.local.com
```

Make sure to add the domain to your hosts file as described above. This resolves the OAuth redirect URI validation issues that occur with `localhost`.

#### npm install fails on NPM 7

If you are using `npm 7` (you can find out the version by typing `npm -v` in your terminal), then you may get the following error when you run `npm install`:
Expand Down
2 changes: 1 addition & 1 deletion packages/create-flex-plugin/src/lib/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const setupConfiguration = async (config: FlexPluginArguments): Promise<F

config.pluginClassName = `${upperFirst(camelCase(name)).replace('Plugin', '')}Plugin`;
config.pluginNamespace = name.toLowerCase().replace('plugin-', '');
config.runtimeUrl = config.runtimeUrl || 'http://localhost:3000';
config.runtimeUrl = config.runtimeUrl || 'http://flex.local.com:3000';
config.targetDirectory = resolveCwd(name);
config.flexSdkVersion = await packages.getLatestFlexUIVersion(2);
config.pluginScriptsVersion = pkg.devDependencies['@twilio/flex-plugin-scripts'];
Expand Down
38 changes: 36 additions & 2 deletions packages/flex-dev-utils/src/__tests__/urls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ describe('urls', () => {

beforeEach(() => {
process.env = { ...OLD_ENV };
delete process.env.DOMAIN; // Ensure clean state
});

it('should return http', () => {
it('should return http with localhost (default)', () => {
const ip = jest.spyOn(address, 'ip').mockReturnValue('192.0.0.0');
const result = urls.getLocalAndNetworkUrls(1234);

Expand All @@ -54,7 +55,23 @@ describe('urls', () => {
ip.mockRestore();
});

it('should return https', () => {
it('should return http with custom domain', () => {
process.env.DOMAIN = 'flex.local.com';
const ip = jest.spyOn(address, 'ip').mockReturnValue('192.0.0.0');
const result = urls.getLocalAndNetworkUrls(1234);

expect(result.local.host).toEqual('0.0.0.0');
expect(result.local.port).toEqual(1234);
expect(result.local.url).toEqual('http://flex.local.com:1234/');

expect(result.network.host).toEqual('192.0.0.0');
expect(result.network.port).toEqual(1234);
expect(result.network.url).toEqual('http://192.0.0.0:1234/');

ip.mockRestore();
});

it('should return https with localhost (default)', () => {
process.env.HTTPS = 'true';
const ip = jest.spyOn(address, 'ip').mockReturnValue('192.0.0.0');
const result = urls.getLocalAndNetworkUrls(1234);
Expand All @@ -69,5 +86,22 @@ describe('urls', () => {

ip.mockRestore();
});

it('should return https with custom domain', () => {
process.env.HTTPS = 'true';
process.env.DOMAIN = 'secure.local.dev';
const ip = jest.spyOn(address, 'ip').mockReturnValue('192.0.0.0');
const result = urls.getLocalAndNetworkUrls(1234);

expect(result.local.host).toEqual('0.0.0.0');
expect(result.local.port).toEqual(1234);
expect(result.local.url).toEqual('https://secure.local.dev:1234/');

expect(result.network.host).toEqual('192.0.0.0');
expect(result.network.port).toEqual(1234);
expect(result.network.url).toEqual('https://192.0.0.0:1234/');

ip.mockRestore();
});
});
});
11 changes: 9 additions & 2 deletions packages/flex-dev-utils/src/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,21 @@ export const findPort = async (startPort: number = 3000): Promise<number> => {
/**
* Returns the local and network urls
* @param port the port the server is running on
* @note Uses configurable domain instead of localhost to avoid SSO v2 OAuth authentication issues
*/
export const getLocalAndNetworkUrls = (port: number): InternalServiceUrls => {
const protocol = env.isHTTPS() ? 'https' : 'http';
const customDomain = env.getDomain();

let localHostname = 'localhost';
if (customDomain) {
localHostname = customDomain;
}

const localUrl = url.format({
protocol,
port,
hostname: 'localhost',
hostname: localHostname,
pathname: '/',
});
const networkUrl = url.format({
Expand All @@ -98,7 +105,7 @@ export const getLocalAndNetworkUrls = (port: number): InternalServiceUrls => {
local: {
url: localUrl,
port,
host: '0.0.0.0',
host: '0.0.0.0', // Keep binding host as 0.0.0.0 for proper network access
},
network: {
url: networkUrl,
Expand Down
2 changes: 1 addition & 1 deletion packages/flex-plugin-e2e-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ TWILIO_ACCOUNT_SID_drawin=ACxxx TWILIO_AUTH_TOKEN_darwin=123 CONSOLE_EMAIL="user
You can also override certain defaults by setting these additional environment variables:

- `FLEX_UI_VERSION` - the flexUIVersion to use. Defaults to `^1` otherwise
- `PLUGIN_BASE_URL` - the baseUrl. Defaults to `http://localhost:3000` otherwise
- `PLUGIN_BASE_URL` - the baseUrl. Defaults to `http://flex.local.com:3000` otherwise
- `TWILIO_REGION` - the twilio region to use

You can also run a specific step by using (don't forget the environment variables):
Expand Down
2 changes: 1 addition & 1 deletion packages/flex-plugin-e2e-tests/src/utils/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export class Browser {

private static _page: Page;

private static _domainsToInclude = ['twilio', 'localhost', 'unpkg'];
private static _domainsToInclude = ['twilio', 'localhost', 'flex.local.com', 'unpkg'];

/**
* Initializes browser object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class Plugins extends Base {

constructor(page: Page, baseUrl: string) {
super(page);
this._baseUrl = baseUrl.includes('localhost') ? 'https://flex.twilio.com' : baseUrl;
this._baseUrl = baseUrl.includes('flex.local.com') ? 'https://flex.twilio.com' : baseUrl;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ export class TwilioConsole extends Base {
}

/**
* Creates a localhost url
* Creates a flex.local.com url
* @param port
*/
private static _createLocalhostUrl = (port: number) => `http://localhost:${port}&localPort=${port}`;
private static _createLocalhostUrl = (port: number) => `http://flex.local.com:${port}&localPort=${port}`;

/**
* Logs user in through service-login
Expand All @@ -39,7 +39,7 @@ export class TwilioConsole extends Base {
*/
async login(flexPath: string, accountSid: string, localhostPort: number, firstLoad: boolean = true): Promise<void> {
logger.info('firstload', firstLoad);
const redirectUrl = this._flexBaseUrl.includes('localhost')
const redirectUrl = this._flexBaseUrl.includes('flex.local.com')
? TwilioConsole._createLocalhostUrl(localhostPort)
: this._flexBaseUrl;
const path = `console/flex/service-login/${accountSid}/?path=/${flexPath}&referer=${redirectUrl}`;
Expand Down
3 changes: 2 additions & 1 deletion packages/flex-plugin-webpack/src/devServer/pluginServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const _getLocalPlugin = (name: string): FlexConfigurationPlugin | undefin
// eslint-disable-next-line import/no-unused-modules
export const _getLocalPlugins = (port: Port, names: string[]): Plugin[] => {
const protocol = `http${env.isHTTPS() ? 's' : ''}://`;
const domain = env.getDomain() || 'localhost';

return names.map((name) => {
const match = _getLocalPlugin(name);
Expand All @@ -63,7 +64,7 @@ export const _getLocalPlugins = (port: Port, names: string[]): Plugin[] => {
return {
phase: 3,
name,
src: `${protocol}localhost:${port}/plugins/${name}.js`,
src: `${protocol}${domain}:${port}/plugins/${name}.js`,
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/flex-plugin-webpack/src/webpack/webpack.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const _getBase = (): Configuration => {
port: env.getPort(),
},
},
host: env.getHost(),
host: env.getHost() || '0.0.0.0', // Keep binding to 0.0.0.0 for network access
port: env.getPort(),
};
};
Expand Down
2 changes: 1 addition & 1 deletion packages/flex-plugins-utils-env/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/* eslint-disable import/no-unused-modules */

export { default, default as env, Environment, Lifecycle, Region } from './lib/env';
export { default, default as env, Environment, Lifecycle, Region, setDomain, getDomain, hasDomain } from './lib/env';
27 changes: 27 additions & 0 deletions packages/flex-plugins-utils-env/src/lib/__tests__/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,33 @@ describe('env', () => {
});
});

describe('domain', () => {
it('should return domain', () => {
process.env.DOMAIN = 'flex.local.com';
expect(env.getDomain()).toEqual('flex.local.com');
});

it('getDomain should return nothing', () => {
expect(env.getDomain()).toEqual(undefined);
});

it('should set domain', () => {
expect(env.getDomain()).toEqual(undefined);
env.setDomain('my-custom.domain');
expect(env.getDomain()).toEqual('my-custom.domain');
});

it('hasDomain should return true when domain is set', () => {
env.setDomain('test.domain');
expect(env.hasDomain()).toBe(true);
});

it('hasDomain should return false when domain is not set', () => {
delete process.env.DOMAIN;
expect(env.hasDomain()).toBe(false);
});
});

describe('port', () => {
it('should return port', () => {
process.env.PORT = '1234';
Expand Down
6 changes: 6 additions & 0 deletions packages/flex-plugins-utils-env/src/lib/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export const getAccountSid = (): string | undefined => getProcessEnv('TWILIO_ACC
export const getAuthToken = (): string | undefined => getProcessEnv('TWILIO_AUTH_TOKEN');
export const hasHost = (): boolean => isDefined(getProcessEnv('HOST'));
export const getHost = (): string | undefined => getProcessEnv('HOST');
export const hasDomain = (): boolean => isDefined(getProcessEnv('DOMAIN'));
export const getDomain = (): string | undefined => getProcessEnv('DOMAIN');
export const setDomain = (domain: string): void => setProcessEnv('DOMAIN', domain);
export const setHost = (host: string): void => setProcessEnv('HOST', host);
export const hasPort = (): boolean => isDefined(getProcessEnv('PORT'));
export const getPort = (): number => Number(getProcessEnv('PORT'));
Expand Down Expand Up @@ -257,6 +260,9 @@ export default {
hasHost,
getHost,
setHost,
hasDomain,
getDomain,
setDomain,
hasPort,
getPort,
setPort,
Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-flex/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@
"flags": {
"name": "The name of the plugin you would like to run. You can provide multiple to run them all concurrently. You can include specific active remote plugins using \"--name 'plugin-name@remote'\" or \"--name 'plugin-name@0.0.0'\" for a specific remote version.",
"includeRemote": "Use this flag to include all remote plugins in your build.",
"port": "The port to start your local development server on."
"port": "The port to start your local development server on.",
"domain": "The domain to start your local development server on. Required when using SSO v2 OAuth flow to avoid authentication issues."
}
},
"flex:plugins:deploy": {
Expand Down
Loading
Loading