medusa-google-login-logic 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,126 @@
1
+ # Medusa Google Login Logic
2
+
3
+ The `medusa-google-login-logic` is a brand new library designed to handle Google OAuth authentication for MedusaJS applications. It is built to be modular, robust, and mirrors production-tested logic (ported from `B&B_ui`).
4
+
5
+ ## 🏗 Architecture
6
+
7
+ The library is split into two layers:
8
+
9
+ 1. **Service Layer (`medusa-services/google-login.ts`)**: Contains pure TypeScript functions for interacting with Medusa API and handling OAuth URL logic.
10
+ 2. **Hook Layer (`google-login-logic/src/hooks/useGoogleAuth.ts`)**: A React hook that orchestrates the flow, manages state (loading, error), and handles UI events.
11
+
12
+ ---
13
+
14
+ ## 🛠 Core Logic Explanation
15
+
16
+ ### 1. The Login Flow (`login` function)
17
+ When you call the `login()` function from the hook:
18
+ - **Base URL Detection**: It automatically detects your storefront URL using environment variables (`NEXT_PUBLIC_BASE_URL` or `NEXT_PUBLIC_MEDUSA_STOREFRONT_URL`) or `window.location.origin`.
19
+ - **Production Routing**: If it detects it's not on `localhost` and matches the `productionDomain` (default: `bellyandbaby.co`), it forces the use of the production URL.
20
+ - **URI Normalization**: It ensures the `redirect_uri` sent to Google matches the Google Console exactly (handles trailing slashes and protocols).
21
+ - **Redirection**: It calls the Medusa SDK to get the Google login location and redirects the user.
22
+
23
+ ### 2. The Callback Flow (`processCallback` function)
24
+ The hook automatically detects the `code` in the URL (if `queryParams` are passed) and completes the login:
25
+ - **Token Exchange**: Exchanges the Google authorization code for a Medusa authentication token.
26
+ - **Account Linking Check**:
27
+ - It decodes the token to check for `actor_id`.
28
+ - If `actor_id` is missing, it means the Google account is not yet linked to a Medusa customer.
29
+ - **Automatic Customer Creation**:
30
+ - For new users, it automatically calls `createCustomer` using the email extracted from the Google token.
31
+ - If the user already has an account but it's not linked, it handles the conflict gracefully and asks for a re-login (`onNeedsReLogin`).
32
+ - **Session Management**: Automatically refreshes the token for existing customers to ensure a valid session and retrieves full customer data.
33
+
34
+ ---
35
+
36
+ ## 🚀 How to Use
37
+
38
+ ### 1. In your Login Button
39
+ ```tsx
40
+ import { useGoogleAuth } from 'medusa-google-login-logic';
41
+
42
+ const GoogleLoginButton = ({ sdk }) => {
43
+ const { login, isLoading, error } = useGoogleAuth({ sdk });
44
+
45
+ return (
46
+ <button onClick={login} disabled={isLoading}>
47
+ {isLoading ? 'Redirecting...' : 'Continue with Google'}
48
+ </button>
49
+ );
50
+ };
51
+ ```
52
+
53
+ ### 2. In your Callback Page
54
+ ```tsx
55
+ import { useGoogleAuth } from 'medusa-google-login-logic';
56
+ import { useSearchParams, useRouter } from 'next/navigation';
57
+
58
+ const GoogleCallbackPage = ({ sdk }) => {
59
+ const searchParams = useSearchParams();
60
+ const queryParams = Object.fromEntries(searchParams.entries());
61
+ const router = useRouter();
62
+
63
+ const { isLoading, error, customer } = useGoogleAuth({
64
+ sdk,
65
+ queryParams, // Hook auto-triggers logic when queryParams.code exists
66
+ onSuccess: (data) => {
67
+ router.push('/account');
68
+ },
69
+ onError: (msg) => alert(msg),
70
+ onNeedsReLogin: (email) => {
71
+ // Typically happens after automatic account creation
72
+ alert("Account created! Please login again with Google.");
73
+ router.push('/login');
74
+ }
75
+ });
76
+
77
+ if (isLoading) return <div>Authenticating...</div>;
78
+ if (error) return <div>Error: {error}</div>;
79
+
80
+ return <div>Welcome back!</div>;
81
+ };
82
+ ```
83
+
84
+ ## ⚙️ Setup for Other Projects
85
+
86
+ To use this library in a new project, follow these steps:
87
+
88
+ ### 1. Installation
89
+ If your project is in the same mono-repo:
90
+ ```bash
91
+ npm install ../path/to/medusa-google-login-logic
92
+ ```
93
+ Or if published:
94
+ ```bash
95
+ npm install medusa-google-login-logic
96
+ ```
97
+
98
+ ### 2. Required Environment Variables
99
+ Add these to your project's `.env` or `.env.local`:
100
+ - `NEXT_PUBLIC_MEDUSA_BACKEND_URL`: Your Medusa backend URL.
101
+ - `NEXT_PUBLIC_BASE_URL`: Your storefront URL (e.g., `https://my-store.com`).
102
+ - `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY`: Your Medusa publishable key.
103
+
104
+ ### 3. Google OAuth Console Configuration
105
+ You MUST add your callback URL to the **Authorized redirect URIs** in your [Google Cloud Console](https://console.cloud.google.com/):
106
+
107
+ - **Local Development**: `http://localhost:8000/auth/customer/google/callback`
108
+ - **Production**: `https://yourdomain.com/auth/customer/google/callback`
109
+
110
+ > [!IMPORTANT]
111
+ > Ensure the URL matches exactly (no trailing slash, matching protocol).
112
+
113
+ ### 4. Code Implementation
114
+ Pass your configured Medusa SDK instance to the hook. The hook handles the rest.
115
+
116
+ ```tsx
117
+ import { Medusa } from "@medusajs/js-sdk";
118
+ import { useGoogleAuth } from "medusa-google-login-logic";
119
+
120
+ const sdk = new Medusa({
121
+ baseUrl: process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL,
122
+ publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
123
+ });
124
+
125
+ // Use in your components as shown in the examples above!
126
+ ```
@@ -0,0 +1,18 @@
1
+ export interface UseGoogleAuthProps {
2
+ sdk: any;
3
+ baseUrl?: string;
4
+ productionDomain?: string;
5
+ callbackPath?: string;
6
+ queryParams?: Record<string, string>;
7
+ onSuccess?: (customer: any) => void;
8
+ onError?: (error: string) => void;
9
+ onNeedsReLogin?: (email: string) => void;
10
+ }
11
+ export declare const useGoogleAuth: ({ sdk, baseUrl: propsBaseUrl, productionDomain, callbackPath, queryParams, onSuccess, onError, onNeedsReLogin }: UseGoogleAuthProps) => {
12
+ login: () => Promise<void>;
13
+ isLoading: boolean;
14
+ error: string | null;
15
+ customer: any;
16
+ needsReLogin: boolean;
17
+ processCallback: () => Promise<void>;
18
+ };
@@ -0,0 +1,2 @@
1
+ export * from './hooks/useGoogleAuth';
2
+ export * from '../../medusa-services/google-login';
@@ -0,0 +1,9 @@
1
+ export interface ContactSubmissionData {
2
+ backendUrl: string;
3
+ publishableKey?: string;
4
+ data: Record<string, any>;
5
+ }
6
+ /**
7
+ * Server-side compatible function to send contact request
8
+ */
9
+ export declare function sendContactRequest({ backendUrl, publishableKey, data }: ContactSubmissionData): Promise<any>;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Normalizes and validates redirect URI to match Google OAuth console exactly
3
+ */
4
+ export declare function normalizeRedirectUri(url: string, isLocalhost: boolean): string;
5
+ export declare const GOOGLE_CALLBACK_PATH = "/auth/customer/google/callback";
6
+ /**
7
+ * Validates redirect URI format matches expected Google OAuth console configuration
8
+ */
9
+ export declare function validateRedirectUri(uri: string, isLocalhost: boolean, productionDomain: string): {
10
+ valid: boolean;
11
+ expected: string;
12
+ actual: string;
13
+ };
14
+ /**
15
+ * Core API logic to initiate Google login
16
+ */
17
+ export declare function performGoogleLogin(sdk: any, redirectUri: string): Promise<any>;
18
+ /**
19
+ * Decodes a JWT token safely
20
+ */
21
+ export declare function decodeToken(token: string): any;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Ported from B&B_ui/src/modules/account/components/google-login-button/actions.ts
3
+ * and B&B_ui/src/app/auth/google/callback/actions.ts
4
+ */
5
+ /**
6
+ * Normalizes and validates redirect URI to match Google OAuth console exactly
7
+ */
8
+ export declare function normalizeRedirectUri(url: string, isLocalhost: boolean): string;
9
+ /**
10
+ * Validates redirect URI format matches expected Google OAuth console configuration
11
+ */
12
+ export declare function validateRedirectUri(uri: string, isLocalhost: boolean, productionDomain: string): {
13
+ valid: boolean;
14
+ expected: string;
15
+ actual: string;
16
+ };
17
+ /**
18
+ * Initiates Google login
19
+ */
20
+ export declare function performGoogleLogin(sdk: any, redirectUri: string): Promise<any>;
21
+ /**
22
+ * Decodes a JWT token safely
23
+ */
24
+ export declare function decodeToken(token: string | null | undefined): any;
25
+ /**
26
+ * Refreshes auth token
27
+ */
28
+ export declare function refreshToken(sdk: any): Promise<string | null>;
29
+ /**
30
+ * Retrieves current customer data
31
+ */
32
+ export declare function retrieveCustomer(sdk: any, token?: string): Promise<any>;
33
+ /**
34
+ * Creates a new customer
35
+ */
36
+ export declare function createCustomer(sdk: any, email: string, token: string): Promise<any>;
@@ -0,0 +1,164 @@
1
+ import { useState as w, useCallback as k, useEffect as z } from "react";
2
+ function C(t, r) {
3
+ try {
4
+ const e = new URL(t);
5
+ return e.pathname = e.pathname.replace(/\/$/, ""), r ? e.protocol = "http:" : e.protocol = "https:", e.toString().replace(/\/$/, "");
6
+ } catch (e) {
7
+ return console.warn("Failed to normalize redirect URI:", e), t.replace(/\/$/, "");
8
+ }
9
+ }
10
+ function M(t, r, e) {
11
+ const o = r ? "http://localhost:8000/auth/customer/google/callback" : `https://${e}/auth/customer/google/callback`, a = C(t, r), f = a === o;
12
+ return f || console.warn("⚠️ Redirect URI validation mismatch:", {
13
+ expected: o,
14
+ actual: a,
15
+ difference: a.replace(o, "") || o.replace(a, "")
16
+ }), {
17
+ valid: f,
18
+ expected: o,
19
+ actual: a
20
+ };
21
+ }
22
+ async function A(t, r) {
23
+ try {
24
+ return await t.auth.login("customer", "google", {
25
+ redirect_uri: r
26
+ });
27
+ } catch (e) {
28
+ throw console.error("❌ Google login initiation failed:", e), e;
29
+ }
30
+ }
31
+ function R(t) {
32
+ if (!t) return null;
33
+ try {
34
+ const r = t.split(".");
35
+ if (r.length !== 3) return null;
36
+ const e = r[1], o = e + "=".repeat((4 - e.length % 4) % 4), a = typeof Buffer < "u" ? Buffer.from(o.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8") : atob(o.replace(/-/g, "+").replace(/_/g, "/"));
37
+ return JSON.parse(a);
38
+ } catch (r) {
39
+ return console.error("Failed to decode token:", r), null;
40
+ }
41
+ }
42
+ async function S(t) {
43
+ try {
44
+ return await t.auth.refresh();
45
+ } catch (r) {
46
+ return console.error("❌ Token refresh failed:", r), null;
47
+ }
48
+ }
49
+ async function x(t, r) {
50
+ try {
51
+ const e = {};
52
+ return r && (e.authorization = `Bearer ${r}`), (await t.client.fetch("/store/customers/me", {
53
+ method: "GET",
54
+ query: {
55
+ fields: "*orders"
56
+ },
57
+ headers: e
58
+ })).customer;
59
+ } catch (e) {
60
+ throw console.error("❌ Customer retrieval failed:", e), e;
61
+ }
62
+ }
63
+ async function E(t, r, e) {
64
+ try {
65
+ return (await t.store.customer.create(
66
+ { email: r },
67
+ {},
68
+ { authorization: `Bearer ${e}` }
69
+ )).customer;
70
+ } catch (o) {
71
+ throw console.error("❌ Customer creation failed:", o), o;
72
+ }
73
+ }
74
+ const y = { BASE_URL: "/", DEV: !1, MODE: "production", PROD: !0, SSR: !1 }, V = ({
75
+ sdk: t,
76
+ baseUrl: r,
77
+ productionDomain: e = "bellyandbaby.co",
78
+ callbackPath: o = "/auth/customer/google/callback",
79
+ queryParams: a,
80
+ onSuccess: f,
81
+ onError: u,
82
+ onNeedsReLogin: h
83
+ }) => {
84
+ const [$, d] = w(!1), [B, p] = w(null), [I, v] = w(null), [G, b] = w(!1), U = (c) => {
85
+ var n;
86
+ try {
87
+ return ((n = process.env) == null ? void 0 : n[c]) || (y == null ? void 0 : y[c]);
88
+ } catch {
89
+ return;
90
+ }
91
+ }, O = k(async () => {
92
+ d(!0), p(null);
93
+ try {
94
+ const c = r || U("NEXT_PUBLIC_MEDUSA_STOREFRONT_URL") || U("NEXT_PUBLIC_BASE_URL") || (typeof window < "u" ? window.location.origin : ""), n = c.includes("localhost") || c.includes("127.0.0.1");
95
+ let i = c;
96
+ !n && e && c.includes(e) && (i = `https://${e}`);
97
+ const l = C(`${i}${o}`, n), s = await A(t, l);
98
+ if (typeof s == "object" && (s != null && s.location)) {
99
+ window.location.href = s.location;
100
+ return;
101
+ }
102
+ if (typeof s != "string")
103
+ throw new Error("Unexpected authentication result");
104
+ window.location.reload();
105
+ } catch (c) {
106
+ console.error("Google login initiation failed:", c);
107
+ const n = c.message || "Failed to initiate Google login";
108
+ p(n), u == null || u(n), d(!1);
109
+ }
110
+ }, [t, r, e, o, u]), m = k(async () => {
111
+ var c, n;
112
+ if (!(!a || !a.code)) {
113
+ d(!0), p(null);
114
+ try {
115
+ const i = await t.auth.callback("customer", "google", a);
116
+ if (typeof i != "string")
117
+ throw new Error("Invalid token response");
118
+ const l = R(i), s = (c = l == null ? void 0 : l.user_metadata) == null ? void 0 : c.email;
119
+ if (!(l == null ? void 0 : l.actor_id)) {
120
+ if (!s)
121
+ throw new Error("No email found in token");
122
+ try {
123
+ await E(t, s, i), b(!0), h == null || h(s), d(!1);
124
+ return;
125
+ } catch (g) {
126
+ if (g.status === 422 || g.status === 409 || (n = g.message) != null && n.includes("already exists")) {
127
+ b(!0), h == null || h(s), d(!1);
128
+ return;
129
+ }
130
+ throw g;
131
+ }
132
+ }
133
+ const _ = await S(t) || i, T = await x(t, _);
134
+ v(T), f == null || f(T);
135
+ } catch (i) {
136
+ console.error("Google callback processing failed:", i);
137
+ const l = i.message || "Authentication failed";
138
+ p(l), u == null || u(l);
139
+ } finally {
140
+ d(!1);
141
+ }
142
+ }
143
+ }, [t, a, f, u, h]);
144
+ return z(() => {
145
+ a && a.code && m();
146
+ }, [a, m]), {
147
+ login: O,
148
+ isLoading: $,
149
+ error: B,
150
+ customer: I,
151
+ needsReLogin: G,
152
+ processCallback: m
153
+ };
154
+ };
155
+ export {
156
+ E as createCustomer,
157
+ R as decodeToken,
158
+ C as normalizeRedirectUri,
159
+ A as performGoogleLogin,
160
+ S as refreshToken,
161
+ x as retrieveCustomer,
162
+ V as useGoogleAuth,
163
+ M as validateRedirectUri
164
+ };
@@ -0,0 +1 @@
1
+ (function(a,l){typeof exports=="object"&&typeof module<"u"?l(exports,require("react")):typeof define=="function"&&define.amd?define(["exports","react"],l):(a=typeof globalThis<"u"?globalThis:a||self,l(a.MedusaGoogleLoginLogic={},a.React))})(this,function(a,l){"use strict";function y(t,r){try{const e=new URL(t);return e.pathname=e.pathname.replace(/\/$/,""),r?e.protocol="http:":e.protocol="https:",e.toString().replace(/\/$/,"")}catch(e){return console.warn("Failed to normalize redirect URI:",e),t.replace(/\/$/,"")}}function I(t,r,e){const o=r?"http://localhost:8000/auth/customer/google/callback":`https://${e}/auth/customer/google/callback`,n=y(t,r),h=n===o;return h||console.warn("⚠️ Redirect URI validation mismatch:",{expected:o,actual:n,difference:n.replace(o,"")||o.replace(n,"")}),{valid:h,expected:o,actual:n}}async function U(t,r){try{return await t.auth.login("customer","google",{redirect_uri:r})}catch(e){throw console.error("❌ Google login initiation failed:",e),e}}function k(t){if(!t)return null;try{const r=t.split(".");if(r.length!==3)return null;const e=r[1],o=e+"=".repeat((4-e.length%4)%4),n=typeof Buffer<"u"?Buffer.from(o.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf-8"):atob(o.replace(/-/g,"+").replace(/_/g,"/"));return JSON.parse(n)}catch(r){return console.error("Failed to decode token:",r),null}}async function C(t){try{return await t.auth.refresh()}catch(r){return console.error("❌ Token refresh failed:",r),null}}async function v(t,r){try{const e={};return r&&(e.authorization=`Bearer ${r}`),(await t.client.fetch("/store/customers/me",{method:"GET",query:{fields:"*orders"},headers:e})).customer}catch(e){throw console.error("❌ Customer retrieval failed:",e),e}}async function G(t,r,e){try{return(await t.store.customer.create({email:r},{},{authorization:`Bearer ${e}`})).customer}catch(o){throw console.error("❌ Customer creation failed:",o),o}}const b={BASE_URL:"/",DEV:!1,MODE:"production",PROD:!0,SSR:!1},R=({sdk:t,baseUrl:r,productionDomain:e="bellyandbaby.co",callbackPath:o="/auth/customer/google/callback",queryParams:n,onSuccess:h,onError:d,onNeedsReLogin:g})=>{const[O,p]=l.useState(!1),[z,w]=l.useState(null),[A,_]=l.useState(null),[j,S]=l.useState(!1),$=c=>{var i;try{return((i=process.env)==null?void 0:i[c])||(b==null?void 0:b[c])}catch{return}},E=l.useCallback(async()=>{p(!0),w(null);try{const c=r||$("NEXT_PUBLIC_MEDUSA_STOREFRONT_URL")||$("NEXT_PUBLIC_BASE_URL")||(typeof window<"u"?window.location.origin:""),i=c.includes("localhost")||c.includes("127.0.0.1");let f=c;!i&&e&&c.includes(e)&&(f=`https://${e}`);const s=y(`${f}${o}`,i),u=await U(t,s);if(typeof u=="object"&&(u!=null&&u.location)){window.location.href=u.location;return}if(typeof u!="string")throw new Error("Unexpected authentication result");window.location.reload()}catch(c){console.error("Google login initiation failed:",c);const i=c.message||"Failed to initiate Google login";w(i),d==null||d(i),p(!1)}},[t,r,e,o,d]),T=l.useCallback(async()=>{var c,i;if(!(!n||!n.code)){p(!0),w(null);try{const f=await t.auth.callback("customer","google",n);if(typeof f!="string")throw new Error("Invalid token response");const s=k(f),u=(c=s==null?void 0:s.user_metadata)==null?void 0:c.email;if(!(s==null?void 0:s.actor_id)){if(!u)throw new Error("No email found in token");try{await G(t,u,f),S(!0),g==null||g(u),p(!1);return}catch(m){if(m.status===422||m.status===409||(i=m.message)!=null&&i.includes("already exists")){S(!0),g==null||g(u),p(!1);return}throw m}}const F=await C(t)||f,B=await v(t,F);_(B),h==null||h(B)}catch(f){console.error("Google callback processing failed:",f);const s=f.message||"Authentication failed";w(s),d==null||d(s)}finally{p(!1)}}},[t,n,h,d,g]);return l.useEffect(()=>{n&&n.code&&T()},[n,T]),{login:E,isLoading:O,error:z,customer:A,needsReLogin:j,processCallback:T}};a.createCustomer=G,a.decodeToken=k,a.normalizeRedirectUri=y,a.performGoogleLogin=U,a.refreshToken=C,a.retrieveCustomer=v,a.useGoogleAuth=R,a.validateRedirectUri=I,Object.defineProperty(a,Symbol.toStringTag,{value:"Module"})});
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "medusa-google-login-logic",
3
+ "version": "1.0.0",
4
+ "description": "Ported Google Login logic from B&B_ui project.",
5
+ "type": "module",
6
+ "main": "./dist/ui-library.umd.cjs",
7
+ "module": "./dist/ui-library.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/ui-library.js",
12
+ "require": "./dist/ui-library.umd.cjs",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "dev": "vite",
21
+ "build": "tsc && vite build",
22
+ "preview": "vite preview"
23
+ },
24
+ "peerDependencies": {
25
+ "react": "^18.0.0 || ^19.0.0",
26
+ "react-dom": "^18.0.0 || ^19.0.0"
27
+ },
28
+ "dependencies": {
29
+ "@medusajs/js-sdk": "^2.11.2"
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20.0.0",
33
+ "@types/react": "^18.0.0 || ^19.0.0",
34
+ "@types/react-dom": "^18.0.0 || ^19.0.0",
35
+ "@vitejs/plugin-react": "^4.0.0",
36
+ "react": "^19.0.0",
37
+ "react-dom": "^19.0.0",
38
+ "typescript": "^5.0.0",
39
+ "vite": "^5.0.0",
40
+ "vite-plugin-dts": "^3.0.0"
41
+ }
42
+ }