import { authorize, unauthorized } from '../core/actions'; import { clearCookie, getCookieValue, getMainJs, resolveEnvAndMerchant } from '../core/utils'; const SHOPIFY_OG_AUTH_ENDPOINT = '/apps/subscriptions/auth/'; const SHOPIFY_OG_AUTH_BEGIN = 'og_auth_begin'; const SHOPIFY_OG_AUTH_END = 'og_auth_end'; type ShopifyOGAuth = { customerId: string; timestamp: number; signature: string; }; /** * We tag shopify liquid this way. * window.ogShopifyConfig = { * {%- if customer -%} * customer: { * id: "{{customer.id}}", * email: "{{customer.email}}", * signature: "{{signature}}", * timestamp: "{{timestamp}}", * }, * {%- else -%} * customer: null, * {%- endif -%} */ type OgShopifyConfigCustomer = { id: string; signature: string; timestamp: string; email?: string; }; type OgShopifyConfig = { customer?: OgShopifyConfigCustomer; }; /** * We rely on window.ogShopifyConfig side of integration */ declare global { interface Window { og: { previewMode: boolean; }; ogShopifyConfig: OgShopifyConfig; Shopify?: { routes?: { root: string; }; currency?: { active: string; rate: string; }; }; } } const parseIntegrationTempAuth = (raw: string) => { const [id, timestamp, signature, email] = atob(raw).split('|'); return { id, signature, timestamp, email } as OgShopifyConfigCustomer; }; /** * * Markup needed for integration: * ```html * * ``` */ export async function authorizeShopifyCustomer({ store }) { const [merchantId] = resolveEnvAndMerchant(); const script = getMainJs(); let customer = script?.dataset.customer ? parseIntegrationTempAuth(script.dataset.customer) : window.ogShopifyConfig?.customer; if (customer) { const val = await getOrCreateAuthCookie(customer); if (val) { const [sig_field, ts, sig] = val.split('|'); store.dispatch(authorize(merchantId, sig_field, Number(ts), sig)); } } else { clearCookie('og_auth'); // after clearing the cookie, we need to make sure the store clears its own auth state store.dispatch(unauthorized('No customer found')); } } /** * Borrow from here https://github.com/ordergroove/shopify-app/blob/88becb621b29776a946ab0cd3ae215043e174626/server/lib/theme-partials/js/helpers/cookies.js#L18 * @param customer * @returns */ export async function fetchOGSignature(customer: OgShopifyConfigCustomer) { try { const response = await fetch( `${SHOPIFY_OG_AUTH_ENDPOINT}?customer=${customer.id}&customer_signature=${customer.signature}&customer_timestamp=${customer.timestamp}` ); const data = await response.text(); const beginningIndex = data.lastIndexOf(SHOPIFY_OG_AUTH_BEGIN); if (beginningIndex < 0) throw 'Invalid response from OG auth endpoint'; return JSON.parse( data.substring(beginningIndex + SHOPIFY_OG_AUTH_BEGIN.length, data.lastIndexOf(SHOPIFY_OG_AUTH_END)) ) as ShopifyOGAuth; } catch (err) { console.error(err); } } /** * Original source https://github.com/ordergroove/shopify-app/blob/88becb621b29776a946ab0cd3ae215043e174626/server/lib/theme-partials/js/helpers/cookies.js#L56 * * @param customer * @returns */ export async function getOrCreateAuthCookie(customer: OgShopifyConfigCustomer) { const ogAuthCookie = getCookieValue('og_auth'); // The cookie hasn't expired yet so we don't need to refresh the auth if (ogAuthCookie) { return ogAuthCookie; } const { customerId, timestamp, signature } = await fetchOGSignature(customer); if (!customerId) return ''; // set expiration to now + 2hrs const ogToday = new Date(); const binarySignature = btoa(signature); ogToday.setTime(ogToday.getTime() + 2 * 60 * 60 * 1000); const value = `${customerId}|${timestamp}|${binarySignature}`; document.cookie = `og_auth=${value};expires=${ogToday.toUTCString()};secure;path=/`; return value; }