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

Commit 4e0877c

Browse files
authored
Merge branch 'main' into remove-package-duplicates
2 parents cafb8f2 + 28fe927 commit 4e0877c

File tree

6 files changed

+221
-4
lines changed

6 files changed

+221
-4
lines changed

.changeset/young-news-fail.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@shopify/shopify-api": minor
3+
---
4+
5+
Add helpers to convert between shop admin URLs and legacy URLs. `sanitizeShop` utility method can now support shop admin URLs.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {shopifyApi} from '../..';
2+
import {testConfig} from '../../__tests__/test-config';
3+
4+
const VALID_URLS = [
5+
{
6+
adminUrl: 'admin.shopify.com/store/my-shop',
7+
legacyAdminUrl: 'my-shop.myshopify.com',
8+
},
9+
{
10+
adminUrl: 'admin.web.abc.def-gh.ij.spin.dev/store/my-shop',
11+
legacyAdminUrl: 'my-shop.shopify.abc.def-gh.ij.spin.dev',
12+
},
13+
];
14+
15+
const INVALID_ADMIN_URLS = [
16+
'not-admin.shopify.com/store/my-shop',
17+
'adminisnotthis.shopify.com/store/my-shop',
18+
'adminisnot.web.abc.def-gh.ij.spin.dev/store/my-shop',
19+
'admin.what.abc.def-gh.ij.spin.dev/store/my-shop',
20+
];
21+
22+
const INVALID_LEGACY_URLS = [
23+
'notshopify.com',
24+
'my-shop.myshopify.com.nope',
25+
'my-shop.myshopify.com/admin',
26+
];
27+
28+
describe.each(VALID_URLS)(
29+
'For valid shop URL: %s',
30+
({adminUrl, legacyAdminUrl}) => {
31+
it('can convert from shop admin URL to legacy URL', () => {
32+
const shopify = shopifyApi(testConfig());
33+
const actual = shopify.utils.shopAdminUrlToLegacyUrl(adminUrl);
34+
expect(actual).toEqual(legacyAdminUrl);
35+
});
36+
37+
it('can convert from legacy URL to shop admin URL', () => {
38+
const shopify = shopifyApi(testConfig());
39+
const actual = shopify.utils.legacyUrlToShopAdminUrl(legacyAdminUrl);
40+
expect(actual).toEqual(adminUrl);
41+
});
42+
43+
it('can strip protocol before converting from shop admin URL to legacy URL', () => {
44+
const shopify = shopifyApi(testConfig());
45+
const urlWithProtocol = `https://${adminUrl}`;
46+
const actual = shopify.utils.shopAdminUrlToLegacyUrl(urlWithProtocol);
47+
expect(actual).toEqual(legacyAdminUrl);
48+
});
49+
50+
it('can strip protocol before converting from legacy URL to shop admin URL', () => {
51+
const shopify = shopifyApi(testConfig());
52+
const urlWithProtocol = `https://${legacyAdminUrl}`;
53+
const actual = shopify.utils.legacyUrlToShopAdminUrl(urlWithProtocol);
54+
expect(actual).toEqual(adminUrl);
55+
});
56+
},
57+
);
58+
59+
describe.each(INVALID_ADMIN_URLS)(
60+
'For invalid shop admin URL: %s',
61+
(invalidUrl) => {
62+
it('returns null when trying to convert from shop admin url to legacy url', () => {
63+
const shopify = shopifyApi(testConfig());
64+
65+
expect(shopify.utils.shopAdminUrlToLegacyUrl(invalidUrl)).toBe(null);
66+
});
67+
},
68+
);
69+
70+
describe.each(INVALID_LEGACY_URLS)(
71+
'For invalid legacy shop URL: %s',
72+
(invalidUrl) => {
73+
it('returns null when trying to convert from legacy url to shop admin url', () => {
74+
const shopify = shopifyApi(testConfig());
75+
76+
expect(shopify.utils.legacyUrlToShopAdminUrl(invalidUrl)).toBe(null);
77+
});
78+
},
79+
);

packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const VALID_SHOP_URL_4 = 'dev-shop-.myshopify.io';
99

1010
const INVALID_SHOP_URL_1 = 'notshopify.com';
1111
const INVALID_SHOP_URL_2 = '-invalid.myshopify.io';
12+
const VALID_SHOPIFY_HOST_BUT_NOT_VALID_ADMIN_URL =
13+
'not-admin.shopify.com/store/my-shop';
1214

1315
const CUSTOM_DOMAIN = 'my-custom-domain.com';
1416
const VALID_SHOP_WITH_CUSTOM_DOMAIN = `my-shop.${CUSTOM_DOMAIN}`;
@@ -28,6 +30,13 @@ const VALID_HOSTS = [
2830
return {testhost, base64host: Buffer.from(testhost).toString('base64')};
2931
});
3032

33+
const VALID_SHOP_ADMIN_URLS = [
34+
{
35+
adminUrl: 'admin.shopify.com/store/my-shop',
36+
legacyAdminUrl: 'my-shop.myshopify.com',
37+
},
38+
];
39+
3140
const INVALID_HOSTS = [
3241
{
3342
testhost: 'plain-string-is-not-base64',
@@ -65,11 +74,14 @@ describe('sanitizeShop', () => {
6574
);
6675
});
6776

68-
test('returns null for invalid URLs', () => {
77+
test.each([
78+
INVALID_SHOP_URL_1,
79+
INVALID_SHOP_URL_2,
80+
VALID_SHOPIFY_HOST_BUT_NOT_VALID_ADMIN_URL,
81+
])('returns null for invalid URL - %s', (invalidUrl) => {
6982
const shopify = shopifyApi(testConfig());
7083

71-
expect(shopify.utils.sanitizeShop(INVALID_SHOP_URL_1)).toBe(null);
72-
expect(shopify.utils.sanitizeShop(INVALID_SHOP_URL_2)).toBe(null);
84+
expect(shopify.utils.sanitizeShop(invalidUrl)).toBe(null);
7385
});
7486

7587
test('throws for invalid URLs if set to', () => {
@@ -104,6 +116,33 @@ describe('sanitizeShop', () => {
104116
shopify.utils.sanitizeShop(INVALID_SHOP_WITH_CUSTOM_DOMAIN_REGEX),
105117
).toBe(null);
106118
});
119+
120+
test.each(VALID_SHOP_ADMIN_URLS)(
121+
'accepts new format of shop admin URLs and converts to legacy admin URLs - %s',
122+
({adminUrl, legacyAdminUrl}) => {
123+
const shopify = shopifyApi(testConfig());
124+
const actual = shopify.utils.sanitizeShop(adminUrl);
125+
126+
expect(actual).toEqual(legacyAdminUrl);
127+
},
128+
);
129+
130+
test('Accepts new format of spin admin URL and converts to legacy admin URL', () => {
131+
const expectedLegacyAdminUrl = 'my-shop.shopify.abc.def-gh.ij.spin.dev';
132+
const spinAdminUrl = 'admin.web.abc.def-gh.ij.spin.dev/store/my-shop';
133+
134+
const shopify = shopifyApi(
135+
testConfig({
136+
customShopDomains: [
137+
'web\\.abc\\.def-gh\\.ij\\.spin\\.dev',
138+
'shopify\\.abc\\.def-gh\\.ij\\.spin\\.dev',
139+
],
140+
}),
141+
);
142+
const actual = shopify.utils.sanitizeShop(spinAdminUrl);
143+
144+
expect(actual).toEqual(expectedLegacyAdminUrl);
145+
});
107146
});
108147

109148
describe('sanitizeHost', () => {

packages/shopify-api/lib/utils/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import {ConfigInterface} from '../base-types';
33
import {sanitizeShop, sanitizeHost} from './shop-validator';
44
import {validateHmac} from './hmac-validator';
55
import {versionCompatible, versionPriorTo} from './version-compatible';
6+
import {
7+
shopAdminUrlToLegacyUrl,
8+
legacyUrlToShopAdminUrl,
9+
} from './shop-admin-url-helper';
610

711
export function shopifyUtils(config: ConfigInterface) {
812
return {
@@ -11,6 +15,8 @@ export function shopifyUtils(config: ConfigInterface) {
1115
validateHmac: validateHmac(config),
1216
versionCompatible: versionCompatible(config),
1317
versionPriorTo: versionPriorTo(config),
18+
shopAdminUrlToLegacyUrl,
19+
legacyUrlToShopAdminUrl,
1420
};
1521
}
1622

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Converts admin.shopify.com/store/my-shop to my-shop.myshopify.com
2+
export function shopAdminUrlToLegacyUrl(shopAdminUrl: string): string | null {
3+
const shopUrl = removeProtocol(shopAdminUrl);
4+
5+
const isShopAdminUrl = shopUrl.split('.')[0] === 'admin';
6+
7+
if (!isShopAdminUrl) {
8+
return null;
9+
}
10+
11+
const regex = new RegExp(`admin\\..+/store/([^/]+)`);
12+
const matches = shopUrl.match(regex);
13+
14+
if (matches && matches.length === 2) {
15+
const shopName = matches[1];
16+
const isSpinUrl = shopUrl.includes('spin.dev/store/');
17+
18+
if (isSpinUrl) {
19+
return spinAdminUrlToLegacyUrl(shopUrl);
20+
} else {
21+
return `${shopName}.myshopify.com`;
22+
}
23+
} else {
24+
return null;
25+
}
26+
}
27+
28+
// Converts my-shop.myshopify.com to admin.shopify.com/store/my-shop
29+
export function legacyUrlToShopAdminUrl(legacyAdminUrl: string): string | null {
30+
const shopUrl = removeProtocol(legacyAdminUrl);
31+
const regex = new RegExp(`(.+)\\.myshopify\\.com$`);
32+
const matches = shopUrl.match(regex);
33+
34+
if (matches && matches.length === 2) {
35+
const shopName = matches[1];
36+
return `admin.shopify.com/store/${shopName}`;
37+
} else {
38+
const isSpinUrl = shopUrl.endsWith('spin.dev');
39+
40+
if (isSpinUrl) {
41+
return spinLegacyUrlToAdminUrl(shopUrl);
42+
} else {
43+
return null;
44+
}
45+
}
46+
}
47+
48+
function spinAdminUrlToLegacyUrl(shopAdminUrl: string) {
49+
const spinRegex = new RegExp(`admin\\.web\\.(.+\\.spin\\.dev)/store/(.+)`);
50+
const spinMatches = shopAdminUrl.match(spinRegex);
51+
52+
if (spinMatches && spinMatches.length === 3) {
53+
const spinUrl = spinMatches[1];
54+
const shopName = spinMatches[2];
55+
return `${shopName}.shopify.${spinUrl}`;
56+
} else {
57+
return null;
58+
}
59+
}
60+
61+
function spinLegacyUrlToAdminUrl(legacyAdminUrl: string) {
62+
const spinRegex = new RegExp(`(.+)\\.shopify\\.(.+\\.spin\\.dev)`);
63+
const spinMatches = legacyAdminUrl.match(spinRegex);
64+
65+
if (spinMatches && spinMatches.length === 3) {
66+
const shopName = spinMatches[1];
67+
const spinUrl = spinMatches[2];
68+
return `admin.web.${spinUrl}/store/${shopName}`;
69+
} else {
70+
return null;
71+
}
72+
}
73+
74+
function removeProtocol(url: string): string {
75+
return url.replace(/^https?:\/\//, '').replace(/\/$/, '');
76+
}

packages/shopify-api/lib/utils/shop-validator.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import {ConfigInterface} from '../base-types';
22
import {InvalidHostError, InvalidShopError} from '../error';
33
import {decodeHost} from '../auth/decode-host';
44

5+
import {shopAdminUrlToLegacyUrl} from './shop-admin-url-helper';
6+
57
export function sanitizeShop(config: ConfigInterface) {
68
return (shop: string, throwOnInvalid = false): string | null => {
9+
let shopUrl = shop;
710
const domainsRegex = ['myshopify\\.com', 'shopify\\.com', 'myshopify\\.io'];
811
if (config.customShopDomains) {
912
domainsRegex.push(
@@ -17,7 +20,16 @@ export function sanitizeShop(config: ConfigInterface) {
1720
`^[a-zA-Z0-9][a-zA-Z0-9-_]*\\.(${domainsRegex.join('|')})[/]*$`,
1821
);
1922

20-
const sanitizedShop = shopUrlRegex.test(shop) ? shop : null;
23+
const shopAdminRegex = new RegExp(
24+
`^admin\\.(${domainsRegex.join('|')})/store/([a-zA-Z0-9][a-zA-Z0-9-_]*)$`,
25+
);
26+
27+
const isShopAdminUrl = shopAdminRegex.test(shopUrl);
28+
if (isShopAdminUrl) {
29+
shopUrl = shopAdminUrlToLegacyUrl(shopUrl) || '';
30+
}
31+
32+
const sanitizedShop = shopUrlRegex.test(shopUrl) ? shopUrl : null;
2133
if (!sanitizedShop && throwOnInvalid) {
2234
throw new InvalidShopError('Received invalid shop argument');
2335
}

0 commit comments

Comments
 (0)