@payez/next-mvp 4.0.27 → 4.0.29

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.
@@ -108,3 +108,21 @@ export declare function getBetterAuthHandler(): Promise<{
108
108
  * This replaces the old databaseHooks approach which doesn't fire in stateless mode.
109
109
  */
110
110
  export declare function exchangeOAuthForIdpTokens(sessionToken: string, provider?: string): Promise<boolean>;
111
+ /**
112
+ * Create a production-ready GET handler for the auth catch-all route.
113
+ *
114
+ * Wraps better-auth's GET handler with:
115
+ * - OAuth state error recovery (redirects to login instead of error page)
116
+ * - IDP token exchange after successful OAuth callback
117
+ *
118
+ * Usage in host app:
119
+ * ```ts
120
+ * import { createAuthGetHandler, getBetterAuthHandler } from '@payez/next-mvp/auth/better-auth';
121
+ * export const GET = createAuthGetHandler('/account-auth/login');
122
+ * export async function POST(req: Request) {
123
+ * const ba = await getBetterAuthHandler();
124
+ * return ba!.POST(req);
125
+ * }
126
+ * ```
127
+ */
128
+ export declare function createAuthGetHandler(loginPath?: string): (request: Request) => Promise<Response>;
@@ -17,6 +17,7 @@ exports.isBetterAuthEnabled = isBetterAuthEnabled;
17
17
  exports.getBetterAuthInstance = getBetterAuthInstance;
18
18
  exports.getBetterAuthHandler = getBetterAuthHandler;
19
19
  exports.exchangeOAuthForIdpTokens = exchangeOAuthForIdpTokens;
20
+ exports.createAuthGetHandler = createAuthGetHandler;
20
21
  require("server-only");
21
22
  const better_auth_1 = require("better-auth");
22
23
  const next_js_1 = require("better-auth/next-js");
@@ -257,3 +258,64 @@ async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
257
258
  return false;
258
259
  }
259
260
  }
261
+ /**
262
+ * Create a production-ready GET handler for the auth catch-all route.
263
+ *
264
+ * Wraps better-auth's GET handler with:
265
+ * - OAuth state error recovery (redirects to login instead of error page)
266
+ * - IDP token exchange after successful OAuth callback
267
+ *
268
+ * Usage in host app:
269
+ * ```ts
270
+ * import { createAuthGetHandler, getBetterAuthHandler } from '@payez/next-mvp/auth/better-auth';
271
+ * export const GET = createAuthGetHandler('/account-auth/login');
272
+ * export async function POST(req: Request) {
273
+ * const ba = await getBetterAuthHandler();
274
+ * return ba!.POST(req);
275
+ * }
276
+ * ```
277
+ */
278
+ function createAuthGetHandler(loginPath = '/account-auth/login') {
279
+ return async function GET(request) {
280
+ const ba = await getBetterAuthHandler();
281
+ if (!ba) {
282
+ return new Response('Auth handler not configured', { status: 500 });
283
+ }
284
+ const response = await ba.GET(request);
285
+ // Intercept auth errors (state mismatch, expired cookies) — redirect to login cleanly
286
+ if (response.status === 302) {
287
+ const location = response.headers.get('location') || '';
288
+ if (location.includes('/api/auth/error') || location.includes('please_restart')) {
289
+ console.warn('[BETTER_AUTH] OAuth state error, redirecting to login');
290
+ return Response.redirect(new URL(loginPath, request.url), 302);
291
+ }
292
+ }
293
+ // After successful OAuth callback: exchange Google identity for IDP tokens
294
+ const url = new URL(request.url);
295
+ if (url.pathname.includes('/callback/') && response.status === 302) {
296
+ try {
297
+ const auth = await getBetterAuthInstance();
298
+ if (auth?.api?.getSession) {
299
+ const setCookies = response.headers.getSetCookie?.() || [];
300
+ const cookieHeader = setCookies
301
+ .map((c) => c.split(';')[0])
302
+ .join('; ');
303
+ const headers = new Headers();
304
+ headers.set('cookie', cookieHeader);
305
+ const session = await auth.api.getSession({ headers });
306
+ if (session?.session?.token) {
307
+ console.log('[BETTER_AUTH] Got session token from callback:', session.session.token.substring(0, 10), '| email:', session.user?.email);
308
+ await exchangeOAuthForIdpTokens(session.session.token);
309
+ }
310
+ else {
311
+ console.warn('[BETTER_AUTH] Could not get session after OAuth callback');
312
+ }
313
+ }
314
+ }
315
+ catch (err) {
316
+ console.error('[BETTER_AUTH] IDP token exchange failed:', err.message);
317
+ }
318
+ }
319
+ return response;
320
+ };
321
+ }
@@ -146,5 +146,5 @@ function EnhancedProfilePage() {
146
146
  contact_info.preferred_contact_method.charAt(0).toUpperCase() +
147
147
  contact_info.preferred_contact_method.slice(1) : undefined, isDarkMode: isDarkMode })] }) }), (0, jsx_runtime_1.jsx)(EditableSection, { title: "Address", isDarkMode: isDarkMode, children: address?.address_line_1 ? ((0, jsx_runtime_1.jsxs)("div", { className: textPrimary, children: [(0, jsx_runtime_1.jsx)("p", { children: address.address_line_1 }), address.address_line_2 && (0, jsx_runtime_1.jsx)("p", { children: address.address_line_2 }), (0, jsx_runtime_1.jsx)("p", { children: [address.city, address.state_name, address.postal_code]
148
148
  .filter(Boolean)
149
- .join(', ') }), (0, jsx_runtime_1.jsx)("p", { children: address.country_name || address.country_code })] })) : ((0, jsx_runtime_1.jsx)("p", { className: textMuted, children: "No address on file" })) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-center gap-6 pt-4", children: [(0, jsx_runtime_1.jsx)("a", { href: "/account/security", className: `text-sm ${isDarkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`, children: "Security Settings \u2192" }), (0, jsx_runtime_1.jsx)("a", { href: "/account/settings", className: `text-sm ${isDarkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`, children: "Preferences \u2192" })] })] }) }));
149
+ .join(', ') }), (0, jsx_runtime_1.jsx)("p", { children: address.country_name || address.country_code })] })) : ((0, jsx_runtime_1.jsx)("p", { className: textMuted, children: "No address on file" })) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-wrap justify-center gap-6 pt-4", children: [(0, jsx_runtime_1.jsx)("a", { href: "/account/subscription", className: `text-sm ${isDarkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`, children: "Subscription & Billing \u2192" }), (0, jsx_runtime_1.jsx)("a", { href: "/account/security", className: `text-sm ${isDarkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`, children: "Security Settings \u2192" }), (0, jsx_runtime_1.jsx)("a", { href: "/account/settings", className: `text-sm ${isDarkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`, children: "Preferences \u2192" })] })] }) }));
150
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "4.0.27",
3
+ "version": "4.0.29",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -283,3 +283,69 @@ export async function exchangeOAuthForIdpTokens(
283
283
  return false;
284
284
  }
285
285
  }
286
+
287
+ /**
288
+ * Create a production-ready GET handler for the auth catch-all route.
289
+ *
290
+ * Wraps better-auth's GET handler with:
291
+ * - OAuth state error recovery (redirects to login instead of error page)
292
+ * - IDP token exchange after successful OAuth callback
293
+ *
294
+ * Usage in host app:
295
+ * ```ts
296
+ * import { createAuthGetHandler, getBetterAuthHandler } from '@payez/next-mvp/auth/better-auth';
297
+ * export const GET = createAuthGetHandler('/account-auth/login');
298
+ * export async function POST(req: Request) {
299
+ * const ba = await getBetterAuthHandler();
300
+ * return ba!.POST(req);
301
+ * }
302
+ * ```
303
+ */
304
+ export function createAuthGetHandler(loginPath: string = '/account-auth/login') {
305
+ return async function GET(request: Request): Promise<Response> {
306
+ const ba = await getBetterAuthHandler();
307
+ if (!ba) {
308
+ return new Response('Auth handler not configured', { status: 500 });
309
+ }
310
+
311
+ const response = await ba.GET(request);
312
+
313
+ // Intercept auth errors (state mismatch, expired cookies) — redirect to login cleanly
314
+ if (response.status === 302) {
315
+ const location = response.headers.get('location') || '';
316
+ if (location.includes('/api/auth/error') || location.includes('please_restart')) {
317
+ console.warn('[BETTER_AUTH] OAuth state error, redirecting to login');
318
+ return Response.redirect(new URL(loginPath, request.url), 302);
319
+ }
320
+ }
321
+
322
+ // After successful OAuth callback: exchange Google identity for IDP tokens
323
+ const url = new URL(request.url);
324
+ if (url.pathname.includes('/callback/') && response.status === 302) {
325
+ try {
326
+ const auth = await getBetterAuthInstance();
327
+ if (auth?.api?.getSession) {
328
+ const setCookies = response.headers.getSetCookie?.() || [];
329
+ const cookieHeader = setCookies
330
+ .map((c: string) => c.split(';')[0])
331
+ .join('; ');
332
+
333
+ const headers = new Headers();
334
+ headers.set('cookie', cookieHeader);
335
+
336
+ const session = await auth.api.getSession({ headers });
337
+ if (session?.session?.token) {
338
+ console.log('[BETTER_AUTH] Got session token from callback:', session.session.token.substring(0, 10), '| email:', session.user?.email);
339
+ await exchangeOAuthForIdpTokens(session.session.token);
340
+ } else {
341
+ console.warn('[BETTER_AUTH] Could not get session after OAuth callback');
342
+ }
343
+ }
344
+ } catch (err: any) {
345
+ console.error('[BETTER_AUTH] IDP token exchange failed:', err.message);
346
+ }
347
+ }
348
+
349
+ return response;
350
+ };
351
+ }
@@ -459,7 +459,13 @@ export default function EnhancedProfilePage() {
459
459
  </EditableSection>
460
460
 
461
461
  {/* Quick Links */}
462
- <div className="flex justify-center gap-6 pt-4">
462
+ <div className="flex flex-wrap justify-center gap-6 pt-4">
463
+ <a
464
+ href="/account/subscription"
465
+ className={`text-sm ${isDarkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}
466
+ >
467
+ Subscription & Billing →
468
+ </a>
463
469
  <a
464
470
  href="/account/security"
465
471
  className={`text-sm ${isDarkMode ? 'text-blue-400 hover:text-blue-300' : 'text-blue-600 hover:text-blue-700'}`}