import {useCallback, useRef} from 'react' import { GeneratedTokenData, UserTokenGenerateUserErrors, } from '@shopify/shop-minis-platform' import {useHandleAction} from '../../internal/useHandleAction' import {useShopActions} from '../../internal/useShopActions' export interface UseGenerateUserTokenReturns { /** * Generates a temporary token for the user. * Tokens are cached in memory and reused if still valid (with a 5-minute expiry buffer). * A new token is automatically generated when the cached token is expired or missing. */ generateUserToken: () => Promise<{ data: GeneratedTokenData userErrors?: UserTokenGenerateUserErrors[] }> } interface CachedTokenResponse { data: GeneratedTokenData userErrors?: UserTokenGenerateUserErrors[] } export function useGenerateUserToken(): UseGenerateUserTokenReturns { const {generateUserToken: generateUserTokenAction} = useShopActions() const wrappedGenerateToken = useHandleAction(generateUserTokenAction) const cachedResponse = useRef(null) const pendingRequest = useRef | null>(null) const isTokenValid = useCallback( (response: CachedTokenResponse | null): boolean => { if (!response?.data?.token || !response.data.expiresAt) { return false } try { const expiryTime = new Date(response.data.expiresAt).getTime() const now = Date.now() // 5 minutes buffer to ensure token doesn't expire mid-request const bufferTime = 5 * 60 * 1000 return expiryTime - bufferTime > now } catch { // If date parsing fails, consider token invalid return false } }, [] ) const generateUserToken = useCallback(async (): Promise => { // Check if cached token exists and is still valid if (cachedResponse.current && isTokenValid(cachedResponse.current)) { return cachedResponse.current } // If there's already a pending request, return the same promise if (pendingRequest.current) { return pendingRequest.current } // Create new request and store the promise pendingRequest.current = (async () => { try { const response = await wrappedGenerateToken() // Only cache if we got a valid token if (response.data?.token && response.data?.expiresAt) { cachedResponse.current = response } return response } catch (error) { // Clear cache on error to ensure fresh token on next attempt cachedResponse.current = null throw error } finally { // Clear pending request after completion (success or failure) pendingRequest.current = null } })() return pendingRequest.current }, [wrappedGenerateToken, isTokenValid]) return { generateUserToken, } } /** * The `useGenerateUserToken` hook generates a temporary token for authenticating the current user with your backend. This token can be verified using the [`userTokenVerify`](/docs/api/shop-minis/minis-admin-api/mutations/usertokenverify) mutation to obtain a permanent user identifier. See [Verifying requests](/docs/api/shop-minis/network-requests#verifying-requests) for implementation details. * * > Note: Some common use cases are: authenticating API requests to your backend, identifying users for personalized experiences, securely linking Shop users to your application's user database. * * * > Note: This hook optionally uses the following scope(s) when declared in the manifest: `openid` * > * > For more details, see [manifest.json](/docs/api/shop-minis/manifest-file). * * @publicDocs */ export type UseGenerateUserTokenGeneratedType = () => UseGenerateUserTokenReturns