Setting the file. One moment.
Client · Wix Headless Templates · wix/skills · Skills Docs
ContentsBack to the top of the page shared/rest/ client.ts
TypeScript · 164 lines · 7 KB
// (what this file does).
14 // server (a Python/PHP/Go port) — the SHOPPER'S SESSION, one token set per shopper. Never one
15 // process-wide token for everyone: that is one shared cart for all visitors.
16 // This is the only part of a port that isn't line-for-line.
17 // docs: https://dev.wix.com/docs/go-headless/authentication/visitors/authenticate-visitors-rest.md
18 import { WIX_CLIENT_ID } from "./config.js" ;
19
20 export const WIX_API_BASE = "https://www.wixapis.com" ;
21 const TOKEN_URL = `${ WIX_API_BASE }/oauth2/token` ;
22 const STORAGE_KEY = `wix-visitor-${ WIX_CLIENT_ID }` ;
23
24 export interface Tokens {
25 accessToken : string ;
26 refreshToken : string ;
27 /** Epoch ms. */
28 expiresAt : number ;
29 /** Whose tokens: absent or "visitor" for an anonymous visitor, "member" after a login (a vertical's rest/auth.ts). */
30 role ?: "visitor" | "member" ;
31 }
32
33 /** Swap this for a server-session-backed store in a port; the browser default is localStorage. */
34 export interface TokenStore {
35 load () : Tokens | null ;
36 save ( tokens : Tokens ) : void ;
37 }
38
39 let memory : Tokens | null = null ;
40 export const browserTokenStore : TokenStore = {
41 load () {
42 if (memory) return memory;
43 if ( typeof localStorage === "undefined" ) return null ;
44 try {
45 const raw = localStorage. getItem ( STORAGE_KEY );
46 if (raw) memory = JSON . parse (raw) as Tokens ;
47 } catch {
48 /* disabled or corrupt storage — mint a fresh visitor */
49 }
50 return memory;
51 },
52 save ( tokens ) {
53 memory = tokens;
54 if ( typeof localStorage === "undefined" ) return ;
55 try {
56 localStorage. setItem ( STORAGE_KEY , JSON . stringify (tokens));
57 } catch {
58 /* storage full or disabled — the session just won't survive a reload */
59 }
60 },
61 };
62 let store : TokenStore = browserTokenStore;
63 /** A port calls this once with its session-backed store. */
64 export function useTokenStore ( s : TokenStore ) : void {
65 store = s;
66 }
67
68 // POST /oauth2/token — grantType "anonymous" mints a visitor; "refresh_token" renews and KEEPS the
69 // identity (a member refresh token yields member tokens); "authorization_code" + codeVerifier turns
70 // a login's authorization code into MEMBER tokens (a vertical's rest/auth.ts). `role` records which.
71 export async function mintTokens ( body : Record < string , string >, role : Tokens [ "role" ] = "visitor" ) : Promise < Tokens > {
72 const res = await fetch ( TOKEN_URL , {
73 method: "POST" ,
74 headers: { "Content-Type" : "application/json" },
75 body: JSON . stringify ({ clientId: WIX_CLIENT_ID , ... body }),
76 });
77 if ( ! res.ok) throw new Error ( `Wix auth failed (${ res . status }).` );
78 const data = await res. json ();
79 return { accessToken: data.access_token, refreshToken: data.refresh_token, expiresAt: Date. now () + data.expires_in * 1000 , role };
80 }
81
82 /** The persisted token set as it is (possibly expired), or null before the first mint. */
83 export function loadTokens () : Tokens | null {
84 return store. load ();
85 }
86
87 /** Adopt a token set — a member's after login. Every call from here on runs as that identity. */
88 export function setTokens ( tokens : Tokens ) : void {
89 store. save (tokens);
90 }
91
92 /** Drop the current identity (logout): the next call mints a fresh anonymous visitor. */
93 export function clearTokens () : void {
94 store. save ({ accessToken: "" , refreshToken: "" , expiresAt: 0 });
95 }
96
97 // The mint or refresh in flight. Calls that start together on a new session (a page loading the
98 // catalog and the cart at once) share it: two anonymous mints are two visitors, two carts.
99 let pending : Promise < string > | null = null ;
100
101 /** A valid access token: cached, refreshed when expired, minted on first use. */
102 export async function accessToken () : Promise < string > {
103 const t = store. load ();
104 if (t && t.expiresAt - 60_000 > Date. now ()) return t.accessToken;
105 pending ??= ( async () => {
106 let fresh : Tokens | undefined ;
107 if (t?.refreshToken) {
108 try {
109 fresh = await mintTokens ({ grantType: "refresh_token" , refreshToken: t.refreshToken }, t.role ?? "visitor" );
110 } catch {
111 /* fall through to a new visitor */
112 }
113 }
114 fresh ??= await mintTokens ({ grantType: "anonymous" });
115 store. save (fresh);
116 return fresh.accessToken;
117 })(). finally (() => {
118 pending = null ;
119 });
120 return pending;
121 }
122
123 export class WixApiError extends Error {
124 /** The response's `details` as Wix sent it (applicationError, validationError) — for callers that map codes. */
125 public details ?: Record < string , any >;
126 constructor ( message : string , public status : number , public code ?: string , details ?: Record < string , any >) {
127 super (message);
128 this .details = details;
129 }
130 }
131
132 /**
133 * Every Wix call goes through here. `path` is relative to WIX_API_BASE; `query` becomes the query
134 * string (an array repeats the key: fields=A&fields=B). Throws WixApiError on a non-2xx, including
135 * 404 — callers that treat 404 as "not found" catch it. The header carries the RAW access token,
136 * no "Bearer " prefix.
137 */
138 export async function wixRequest < T = any >(
139 path : string ,
140 { method = "POST" , body , query } : { method ?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" ; body ?: unknown ; query ?: Record < string , string | readonly string []> } = {},
141 ) : Promise < T > {
142 const url = new URL (path, WIX_API_BASE );
143 for ( const [ k , v ] of Object. entries (query ?? {})) {
144 for ( const item of Array. isArray (v) ? v : [v as string ]) url.searchParams. append (k, String (item));
145 }
146 const res = await fetch (url, {
147 method,
148 headers: { Authorization: await accessToken (), "Content-Type" : "application/json" },
149 body: body === undefined ? undefined : JSON . stringify (body),
150 });
151 if ( ! res.ok) {
152 let message = "" , code : string | undefined , details : Record < string , any > | undefined ;
153 try {
154 const j = await res. json ();
155 message = j?.message ?? "" ;
156 code = j?.details?.applicationError?.code;
157 details = j?.details;
158 } catch {
159 /* no JSON body */
160 }
161 throw new WixApiError (message || `${ method } ${ url . pathname } failed (${ res . status }).` , res.status, code, details);
162 }
163 return (res.status === 204 ? null : await res. json ()) as T ;
164 }