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

Add a new utility for constructing the host URL for the app. #419

Merged
merged 4 commits into from
Jul 14, 2022
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased

- Add new method to construct the host app URL [#419](https://github.com/Shopify/shopify-api-node/pull/419)

## [4.0.0] - 2022-07-04

- ⚠️ [Breaking] Add REST resources for July 2022 API version, add `LATEST_API_VERSION` constant, remove support and REST resources for July 2021 (`2021-07`) API version [#415](https://github.com/Shopify/shopify-node-api/pull/415)
Expand Down
14 changes: 14 additions & 0 deletions docs/usage/utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# utils

## Get the application URL for embedded apps using `getEmbeddedAppUrl`

If you need to redirect a request to your embedded app URL you can use `getEmbeddedAppUrl`

```ts
const redirectURL = getEmbeddedAppUrl(request);
res.redirect(redirectURL);
```

Using this utility ensures that embedded app URL is properly constructed and brings the merchant to the right place. It is more reliable than using the shop param.

This utility relies on the host query param being a Base 64 encoded string. All requests from Shopify should include this param in the correct format.
byrichardpowell marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,5 @@ export class SessionStorageError extends ShopifyError {}

export class MissingRequiredArgument extends ShopifyError {}
export class UnsupportedClientType extends ShopifyError {}

export class InvalidRequestError extends ShopifyError {}
57 changes: 57 additions & 0 deletions src/utils/__tests__/get-embedded-app-url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import http from 'http';

import getEmbeddedAppUrl from '../get-embedded-app-url';
import * as ShopifyErrors from '../../error';
import {Context} from '../../context';

describe('getEmbeddedAppUrl', () => {
beforeEach(() => {
Context.API_KEY = 'my-api-key';
});

test('throws an error when no request is passed', () => {
// @ts-expect-error: For JS users test it throws when no request is passed
expect(() => getEmbeddedAppUrl()).toThrow(
ShopifyErrors.MissingRequiredArgument,
);
});

test('throws an error when the request has no URL', () => {
const req = {
url: undefined,
} as http.IncomingMessage;

expect(() => getEmbeddedAppUrl(req)).toThrow(
ShopifyErrors.InvalidRequestError,
);
});

test('throws an error when the request has no host query param', () => {
const req = {
url: '/?shop=test.myshopify.com',
headers: {
host: 'test.myshopify.com',
},
} as http.IncomingMessage;

expect(() => getEmbeddedAppUrl(req)).toThrow(
ShopifyErrors.InvalidRequestError,
);
});

test('returns the host app url', () => {
const host = 'test.myshopify.com/admin';
const base64Host = Buffer.from(host, 'utf-8').toString('base64');

const req = {
url: `?shop=test.myshopify.com&host=${base64Host}`,
headers: {
host: 'test.myshopify.com',
},
} as http.IncomingMessage;

expect(getEmbeddedAppUrl(req)).toBe(
`https://${host}/apps/${Context.API_KEY}`,
);
});
});
38 changes: 38 additions & 0 deletions src/utils/get-embedded-app-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import http from 'http';

import * as ShopifyErrors from '../error';
import {Context} from '../context';

/**
* Helper method to get the host URL for the app.
*
* @param request Current HTTP request
*/
export default function getEmbeddedAppUrl(
request: http.IncomingMessage,
): string {
if (!request) {
throw new ShopifyErrors.MissingRequiredArgument(
'getEmbeddedAppUrl requires a request object argument',
);
}

if (!request.url) {
throw new ShopifyErrors.InvalidRequestError(
'Request does not contain a URL',
);
}

const url = new URL(request.url, `https://${request.headers.host}`);
const host = url.searchParams.get('host');

if (typeof host !== 'string') {
throw new ShopifyErrors.InvalidRequestError(
'Request does not contain a host query parameter',
);
}

const decodedHost = Buffer.from(host, 'base64').toString();

return `https://${decodedHost}/apps/${Context.API_KEY}`;
}
2 changes: 2 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import validateHmac from './hmac-validator';
import validateShop from './shop-validator';
import versionCompatible from './version-compatible';
import withSession from './with-session';
import getEmbeddedAppUrl from './get-embedded-app-url';

const ShopifyUtils = {
decodeSessionToken,
Expand All @@ -26,6 +27,7 @@ const ShopifyUtils = {
validateShop,
versionCompatible,
withSession,
getEmbeddedAppUrl,
};

export default ShopifyUtils;