Skip to content

Commit 71913a4

Browse files
committed
pos provider auth
1 parent 90f9e51 commit 71913a4

28 files changed

+1255
-518
lines changed

src/auth/AuthDebug.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import {useAuth} from "./authContext.ts";
22

33
export const AuthDebug = () => {
4-
const {user, pos, isAuthenticated} = useAuth();
4+
const {user, currentProvider, currentPosToken, isAuthenticated} = useAuth();
55
return (
66
<pre className="text-xs">
7-
{JSON.stringify({ isAuthenticated, user, pos }, null, 2)}
7+
{JSON.stringify({ isAuthenticated, user, currentProvider, currentPosToken }, null, 2)}
88
</pre>
99
);
1010
}

src/auth/AuthProvider.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {authMachine} from "./authMachine";
55
import {useAuthService} from "./useAuthService.ts";
66
import {useGoogleOidc} from "./google";
77
import {useSquareOAuth} from "./square.ts";
8+
import {useCloverOAuth} from "./clover.ts";
9+
import {useShopifyOAuth} from "./shopify.ts";
810
import Spinner from "../components/common/Spinner";
9-
import useRetailerOnboarding from "../users/retailer/useRetailerOnboarding.ts";
1011

1112
export const AuthProvider = ({children}: { children: ReactNode }) => {
1213

@@ -28,12 +29,23 @@ export const AuthProvider = ({children}: { children: ReactNode }) => {
2829
square.authenticate();
2930
}, [square]);
3031

31-
// Implicit retailer onboarding: runs once when Square is authorized
32-
const squareToken = auth.pos.square;
33-
const isAuthorized = !!squareToken && new Date(squareToken.expires_at).getTime() > Date.now();
34-
const retailerId = auth.user?.user.role.id ?? null;
35-
const merchantId = squareToken?.merchant_id ?? null;
36-
useRetailerOnboarding({retailerId, merchantId, isAuthorized});
32+
// Handle Clover OAuth
33+
const clover = useCloverOAuth({auth});
34+
useEffect(() => {
35+
const pending = sessionStorage.getItem("clover_oauth_pending");
36+
if (!pending) return;
37+
clover.authenticate();
38+
}, [clover]);
39+
40+
// Handle Shopify OAuth
41+
const shopify = useShopifyOAuth({auth});
42+
useEffect(() => {
43+
const pending = sessionStorage.getItem("shopify_oauth_pending");
44+
if (!pending) return;
45+
shopify.authenticate();
46+
}, [shopify]);
47+
48+
// NOTE: Do not trigger onboarding automatically on page load. Onboarding is a one-time, explicit flow.
3749

3850
// CORRECT: matches({ authenticated: "idle" })
3951
if (state.matches("loading") || google.isProcessing) {

src/auth/authClient.ts

Lines changed: 72 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export const saveRole = async (
6464
): Promise<SessionUser> => {
6565
const token = storage.getToken();
6666
// Backend expects RoleEnum, which is typically uppercase. Send uppercase to avoid 400.
67-
const body = { role: role.toUpperCase() } as unknown as { role: string };
67+
const body = {role: role.toUpperCase()} as unknown as { role: string };
6868
const {data} = await api.patch(
6969
"/session/user",
7070
body,
@@ -73,28 +73,87 @@ export const saveRole = async (
7373
return data;
7474
};
7575

76-
export const startAuthorization = (userId: string): void => {
76+
export const startAuthorization = (provider: "square" | "clover" | "shopify", userId: string, shop: string | null): void => {
7777
if (!userId) {
78-
console.warn("Cannot start Square OAuth: missing userId");
78+
console.warn(`Cannot start ${provider} OAuth: missing userId`);
7979
return;
8080
}
81-
sessionStorage.setItem("square_oauth_pending", "true");
82-
window.location.assign(`${BASE_URL}/square/authorize?id=${userId}`);
81+
const key = `${provider}_oauth_pending`;
82+
sessionStorage.setItem(key, "true");
83+
84+
// For Shopify, pass the shop value as provided by the user. Backend will normalize it.
85+
if (provider === "shopify") {
86+
const provided = shop ?? "";
87+
if (!provided) {
88+
console.warn("Shopify OAuth requires a shop value");
89+
sessionStorage.removeItem(key);
90+
return;
91+
}
92+
// Swagger: /{provider}/authorize?userId=...&shop=...
93+
window.location.assign(`${BASE_URL}/${provider}/authorize?user_id=${encodeURIComponent(userId)}&shop=${encodeURIComponent(provided)}`);
94+
return;
95+
}
96+
97+
// Square / Clover don't require extra params
98+
// Swagger: /{provider}/authorize?userId=...
99+
window.location.assign(`${BASE_URL}/${provider}/authorize?user_id=${encodeURIComponent(userId)}`);
83100
};
84101

85102
// === POS: Only status & refresh ===
86-
export const getPosToken = async (provider: "square" | "clover", merchantId?: string | null) => {
103+
export const getPosToken = async (provider: "square" | "clover" | "shopify", merchantId?: string | null) => {
87104
if (!merchantId) {
88-
console.error("NO MERCHANT ID for:", provider);
105+
console.warn(`[pos] Skipping ${provider} token load: missing merchantId`);
106+
return null;
89107
}
90-
const {data} = await api.get(`/${provider}/status`, {params: {merchant_id: merchantId}});
91-
return data;
108+
// Swagger: GET /{provider}/token?merchantId=...
109+
const {data} = await api.get(`/${provider}/token`, {params: {merchant_id: merchantId}});
110+
111+
if (!data) return null;
112+
const expiry = pickExpiryMs(data);
113+
114+
return {
115+
merchantId: data.merchantId ?? data.merchant_id ?? "",
116+
expiry,
117+
token: data.token ?? undefined,
118+
} as { merchantId: string; expiry: number | null; token?: string };
92119
};
93120

94-
export const refreshPosToken = async (provider: "square" | "clover", merchantId?: string | null) => {
121+
export const refreshPosToken = async (provider: "square" | "clover" | "shopify", merchantId?: string | null) => {
95122
if (!merchantId) {
96-
console.error("NO MERCHANT ID for:", provider);
123+
console.warn(`[pos] Skipping ${provider} token refresh: missing merchantId`);
124+
return null;
97125
}
126+
// Swagger: POST /{provider}/refresh?merchantId=...
98127
const {data} = await api.post(`/${provider}/refresh`, null, {params: {merchant_id: merchantId}});
99-
return data;
100-
};
128+
if (!data) return null;
129+
const expiry = pickExpiryMs(data);
130+
return {
131+
merchantId: data.merchantId ?? data.merchant_id ?? "",
132+
expiry,
133+
token: data.token ?? undefined,
134+
} as { merchantId: string; expiry: number | null; token?: string };
135+
};
136+
137+
// === Expiry selection: backend exposes multiple fields; prefer ms when present ===
138+
function pickExpiryMs(data: any): number | null {
139+
// 1) Prefer numeric epoch ms directly
140+
const ms = data?.expiresAtMs;
141+
if (typeof ms === "number" && Number.isFinite(ms) && ms > 0) return Math.round(ms);
142+
143+
// 2) If ISO string Instant exists, parse it
144+
const iso = data?.expiresAt ?? "";
145+
if (typeof iso === "string" && iso.trim()) {
146+
// Trim fractional seconds beyond ms precision to avoid parse inconsistencies
147+
const trimmed = iso.replace(/\.(\d{3})\d+(Z|[+\-]\d{2}:\d{2})$/, ".$1$2");
148+
const parsed = Date.parse(trimmed);
149+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
150+
}
151+
152+
// 3) As a fallback, if seconds TTL provided, convert relative seconds to absolute ms
153+
const secs = data?.expiresInSeconds;
154+
if (typeof secs === "number" && Number.isFinite(secs) && secs > 0) {
155+
return Date.now() + Math.round(secs * 1000);
156+
}
157+
158+
return null;
159+
}

src/auth/authContext.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
1-
import {createContext, useContext} from "react";
2-
import type {AuthContextValue} from "./types";
1+
import { createContext, useContext } from "react";
2+
import { useAuthService } from "./useAuthService";
33

4-
export const AuthContext = createContext<AuthContextValue>(null as never);
4+
// Infer the exact return type from useAuthService — no duplication!
5+
export type AuthContextValue = ReturnType<typeof useAuthService>;
6+
7+
export const AuthContext = createContext<AuthContextValue | null>(null);
58

69
/**
710
* Custom hook to access auth context values.
8-
* Loads XState machine into the React context.
911
*/
1012
export const useAuth = (): AuthContextValue => {
1113
const ctx = useContext(AuthContext);
12-
if (!ctx) throw new Error("useAuth must be used within an AuthProvider");
14+
if (!ctx) {
15+
throw new Error("useAuth must be used within an AuthProvider");
16+
}
1317
return ctx;
1418
};

0 commit comments

Comments
 (0)