Skip to content

Commit 02268bb

Browse files
committed
feat(shopify): expand operations and refresh expiring credentials
1 parent 146be34 commit 02268bb

48 files changed

Lines changed: 6133 additions & 842 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/docs/content/docs/integrations/shopify.mdx‎

Lines changed: 685 additions & 91 deletions
Large diffs are not rendered by default.

‎apps/sim/app/api/auth/oauth2/callback/shopify/route.test.ts‎

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,6 @@ describe('Shopify OAuth callback', () => {
5858
},
5959
})
6060
mockCompleteShopifyOAuthConnection.mockResolvedValue(undefined)
61-
vi.stubGlobal(
62-
'fetch',
63-
vi.fn().mockImplementation(() =>
64-
Promise.resolve(
65-
new Response(JSON.stringify({ access_token: 'shopify-token', scope: 'read_products' }), {
66-
status: 200,
67-
headers: { 'Content-Type': 'application/json' },
68-
})
69-
)
70-
)
71-
)
7261
})
7362

7463
it('completes the credential draft carried by signed state instead of a shared cookie', async () => {
@@ -83,9 +72,8 @@ describe('Shopify OAuth callback', () => {
8372
const response = await GET(callbackRequest(state))
8473

8574
expect(mockCompleteShopifyOAuthConnection).toHaveBeenCalledWith({
86-
accessToken: 'shopify-token',
75+
code: 'authorization-code',
8776
shopDomain: SHOP_DOMAIN,
88-
scope: 'read_products',
8977
userId: 'user-1',
9078
draftId: 'draft-from-state',
9179
signal: expect.any(AbortSignal),

‎apps/sim/app/api/auth/oauth2/callback/shopify/route.ts‎

Lines changed: 9 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { isSameOrigin } from '@/lib/core/utils/validation'
1414
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1515
import { APP_ENTRY_PATH } from '@/lib/navigation/paths'
1616
import { completeShopifyOAuthConnection } from '@/lib/oauth/shopify'
17+
import { ShopifyOAuthError } from '@/lib/oauth/shopify-installation'
1718
import { parseShopifyOAuthState } from '@/lib/oauth/shopify-state'
1819

1920
const logger = createLogger('ShopifyCallback')
@@ -75,7 +76,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
7576
})
7677

7778
const {
78-
values: { SHOPIFY_CLIENT_ID: clientId, SHOPIFY_CLIENT_SECRET: clientSecret },
79+
values: { SHOPIFY_CLIENT_SECRET: clientSecret },
7980
} = requireConfiguredOAuthClient('shopify')
8081

8182
if (!validateHmac(searchParams, clientSecret)) {
@@ -111,64 +112,31 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
111112
clientSecret,
112113
})
113114

114-
const tokenResponse = await fetch(`https://${shopDomain}/admin/oauth/access_token`, {
115-
method: 'POST',
116-
headers: {
117-
'Content-Type': 'application/json',
118-
},
119-
body: JSON.stringify({
120-
client_id: clientId,
121-
client_secret: clientSecret,
122-
code: code,
123-
}),
124-
})
125-
126-
if (!tokenResponse.ok) {
127-
const errorText = await tokenResponse.text()
128-
logger.error('Failed to exchange code for token:', {
129-
status: tokenResponse.status,
130-
body: errorText,
131-
})
132-
return NextResponse.redirect(`${baseUrl}${APP_ENTRY_PATH}?error=shopify_token_error`)
133-
}
134-
135-
const tokenData = await tokenResponse.json()
136-
const accessToken = tokenData.access_token
137-
const scope = tokenData.scope
138-
139-
logger.info('Shopify token exchange successful:', {
140-
hasAccessToken: !!accessToken,
141-
scope: scope,
142-
})
143-
144-
if (!accessToken) {
145-
logger.error('No access token in response')
146-
return NextResponse.redirect(`${baseUrl}${APP_ENTRY_PATH}?error=shopify_no_token`)
115+
if (returnUrl && !isSameOrigin(returnUrl)) {
116+
throw new Error('Shopify OAuth state contains an invalid return URL')
147117
}
148118

149119
await completeShopifyOAuthConnection({
150-
accessToken,
120+
code,
151121
shopDomain,
152-
scope,
153122
userId: session.user.id,
154123
draftId,
155124
signal: request.signal,
156125
})
157126

158-
if (returnUrl && !isSameOrigin(returnUrl)) {
159-
throw new Error('Shopify OAuth state contains an invalid return URL')
160-
}
161127
const redirectUrl = returnUrl ?? `${baseUrl}${APP_ENTRY_PATH}`
162128
const finalUrl = new URL(redirectUrl)
163129
finalUrl.searchParams.set('shopify_connected', 'true')
164130

165131
return clearShopifyOAuthCookies(NextResponse.redirect(finalUrl))
166132
} catch (error) {
167-
logger.error('Error in Shopify OAuth callback:', error)
133+
logger.error('Shopify OAuth callback failed')
168134
const errorCode =
169135
error instanceof EnvCapabilityConfigurationError && error.capabilityId === 'oauth'
170136
? 'shopify_config_error'
171-
: 'shopify_callback_error'
137+
: error instanceof ShopifyOAuthError
138+
? error.callbackError
139+
: 'shopify_callback_error'
172140
return clearShopifyOAuthCookies(
173141
NextResponse.redirect(`${baseUrl}${APP_ENTRY_PATH}?error=${errorCode}`)
174142
)

0 commit comments

Comments
 (0)