[GH-ISSUE #7791] Expo OAuth fails with "device id and device name are required for private api" when using network IP #19537

Closed
opened 2026-04-15 18:46:33 -05:00 by GiteaMirror · 8 comments
Owner

Originally created by @issam-seghir on GitHub (Feb 4, 2026).
Original GitHub issue: https://github.com/better-auth/better-auth/issues/7791

Originally assigned to: @bytaesu on GitHub.

Issue Summary

When using the Expo plugin with OAuth (Google) and following the recommended pattern of using a network IP for OAuth callbacks (to work with external browsers), the authentication fails with:

Error 400: Invalid request
device id and device name are required for private api

Environment

  • Better Auth: Latest (with @better-auth/expo plugin)
  • Expo SDK: 54+
  • OAuth Provider: Google
  • Framework: React Native with Expo Router
  • Development: Local network (192.168.x.x)

Steps to Reproduce

  1. Configure Better Auth with OAuth Proxy:

    // packages/auth/src/index.ts
    export function initAuth(options) {
        return betterAuth({
            baseURL: options.productionUrl,  // Using network IP for OAuth
            plugins: [
                oAuthProxy(),
                expo(),
            ],
            socialProviders: {
                google: {
                    clientId: "...",
                    clientSecret: "...",
                    redirectURI: `${options.productionUrl}/api/auth/callback/google`,
                },
            },
        });
    }
    
  2. Server uses network IP in development:

    // apps/nextjs/src/auth/server.ts
    const productionUrl = process.env.NODE_ENV === "development"
        ? `http://192.168.x.x:3000`  // Network IP for OAuth callback
        : `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
    
    export const auth = initAuth({
        baseUrl: "http://localhost:3000",
        productionUrl,
        googleClientId: env.AUTH_GOOGLE_ID,
        googleClientSecret: env.AUTH_GOOGLE_SECRET,
    });
    
  3. Expo client configured correctly:

    // apps/expo/src/utils/auth.ts
    export const authClient = createAuthClient({
        baseURL: getBaseUrl(),  // Returns http://192.168.x.x:3000
        plugins: [
            expoClient({
                scheme: "creativebook",
                storage: SecureStore,
            }),
        ],
    });
    
  4. Attempt Google sign-in from Expo app

  5. OAuth flow initiates correctly:

    • App opens browser with correct authorization URL
    • User authenticates with Google
    • Google redirects to callback: http://192.168.x.x:3000/api/auth/callback/google
  6. Error occurs:

    Error 400: Invalid request
    device id and device name are required for private api
    

Expected Behavior

OAuth should work seamlessly with Expo when using network IP for callbacks, with the Expo plugin handling device information requirements automatically (if needed).

Actual Behavior

Better Auth returns error 400 stating device id and device name are required for "private api".

Questions

  1. Are we missing something in the configuration?

    • Is there an undocumented option or pattern we should follow?
    • Should device info be provided somewhere in the auth config?
  2. Should the Expo plugin handle this automatically?

    • Should the expo() plugin detect OAuth flows and handle device info requirements under the hood?
    • Or should it pass device info from the Expo client to the server?
  3. Should there be a configuration option?

    • Would it make sense to add an option like requireDeviceInfo: boolean to the Expo plugin?
    • Or a way to bypass device info checks for OAuth flows (since they originate from external browsers)?

Additional Context

OAuth Flow Diagram

Expo App (creativebook://)
    ↓ (1) authClient.signIn.social({ provider: "google" })
    ↓
Next.js Server (http://192.168.x.x:3000)
    ↓ (2) Generates OAuth URL with callback
    ↓
External Browser (Chrome/Safari)
    ↓ (3) User authenticates with Google
    ↓ (4) Google redirects to callback URL
    ↓
Next.js Server (http://192.168.x.x:3000/api/auth/callback/google)
    ❌ (5) ERROR: device id and device name required

Why Network IP?

We use network IP instead of localhost because:

  • OAuth callbacks come from external browser (Chrome/Safari)
  • External browser cannot reach localhost:3000
  • Network IP (192.168.x.x:3000) is accessible from the device's browser

Expo Plugin Context

The Expo plugin documentation mentions it's for "mobile device management" but doesn't clarify:

  • How device info should be provided during OAuth flows
  • Whether OAuth flows should bypass device info requirements
  • How the plugin interacts with the OAuth proxy

Workaround Attempted

We tried various configurations but none resolved the issue:

  1. Using baseURL as network IP (current approach)
  2. Removing explicit redirectURI (didn't help)
  3. Adding trustedOrigins with Expo schemes (still fails)

This appears related to the broader challenge of using Better Auth OAuth with Expo apps in development, where:

  • Development servers use dynamic network IPs
  • OAuth callbacks must be accessible from external browsers
  • Mobile apps need different handling than web apps

Environment Details

Server logs:

🔐 Better Auth OAuth Config:
   baseUrl: http://localhost:3000
   productionUrl: http://192.168.x.x:3000
   ✅ OAuth callbacks will redirect to: http://192.168.x.x:3000/api/auth/callback/*

Expo logs:

📱 Expo Base URL Config:
   debuggerHost: 192.168.x.x:8081
   hostIp: 192.168.x.x
   baseUrl: http://192.168.x.x:3000

OAuth authorization URL (works correctly):

https://accounts.google.com/o/oauth2/auth?
  response_type=code
  &client_id=...
  &redirect_uri=http://192.168.x.x:3000/api/auth/callback/google
  ...

Proposed Solutions

  1. Option A: Add requireDeviceInfo: boolean to Expo plugin options

    expo({
        requireDeviceInfo: false,  // Disable for OAuth flows
    })
    
  2. Option B: Auto-detect OAuth flows and skip device info

    • Expo plugin automatically bypasses device checks when request comes from OAuth proxy
  3. Option C: Allow device info to be optional in specific contexts

    • Document how to provide device info from Expo client during OAuth
  4. Option D: Documentation clarification

    • If this is a configuration issue, document the correct pattern for Expo + OAuth

Request

Could you please clarify:

  • Is this expected behavior or a bug?
  • What's the recommended pattern for Expo + OAuth in development?
  • Should device info be provided, and if so, how?

Full configuration details: https://github.com/issam-seghir/creative-book-mobile/blob/main/docs/BETTER_AUTH_OAUTH_ISSUE.md

Originally created by @issam-seghir on GitHub (Feb 4, 2026). Original GitHub issue: https://github.com/better-auth/better-auth/issues/7791 Originally assigned to: @bytaesu on GitHub. ## Issue Summary When using the Expo plugin with OAuth (Google) and following the recommended pattern of using a network IP for OAuth callbacks (to work with external browsers), the authentication fails with: ``` Error 400: Invalid request device id and device name are required for private api ``` ## Environment - **Better Auth**: Latest (with @better-auth/expo plugin) - **Expo SDK**: 54+ - **OAuth Provider**: Google - **Framework**: React Native with Expo Router - **Development**: Local network (192.168.x.x) ## Steps to Reproduce 1. **Configure Better Auth with OAuth Proxy:** ```typescript // packages/auth/src/index.ts export function initAuth(options) { return betterAuth({ baseURL: options.productionUrl, // Using network IP for OAuth plugins: [ oAuthProxy(), expo(), ], socialProviders: { google: { clientId: "...", clientSecret: "...", redirectURI: `${options.productionUrl}/api/auth/callback/google`, }, }, }); } ``` 2. **Server uses network IP in development:** ```typescript // apps/nextjs/src/auth/server.ts const productionUrl = process.env.NODE_ENV === "development" ? `http://192.168.x.x:3000` // Network IP for OAuth callback : `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`; export const auth = initAuth({ baseUrl: "http://localhost:3000", productionUrl, googleClientId: env.AUTH_GOOGLE_ID, googleClientSecret: env.AUTH_GOOGLE_SECRET, }); ``` 3. **Expo client configured correctly:** ```typescript // apps/expo/src/utils/auth.ts export const authClient = createAuthClient({ baseURL: getBaseUrl(), // Returns http://192.168.x.x:3000 plugins: [ expoClient({ scheme: "creativebook", storage: SecureStore, }), ], }); ``` 4. **Attempt Google sign-in from Expo app** 5. **OAuth flow initiates correctly:** - App opens browser with correct authorization URL - User authenticates with Google - Google redirects to callback: `http://192.168.x.x:3000/api/auth/callback/google` 6. **Error occurs:** ``` Error 400: Invalid request device id and device name are required for private api ``` ## Expected Behavior OAuth should work seamlessly with Expo when using network IP for callbacks, with the Expo plugin handling device information requirements automatically (if needed). ## Actual Behavior Better Auth returns error 400 stating device id and device name are required for "private api". ## Questions 1. **Are we missing something in the configuration?** - Is there an undocumented option or pattern we should follow? - Should device info be provided somewhere in the auth config? 2. **Should the Expo plugin handle this automatically?** - Should the `expo()` plugin detect OAuth flows and handle device info requirements under the hood? - Or should it pass device info from the Expo client to the server? 3. **Should there be a configuration option?** - Would it make sense to add an option like `requireDeviceInfo: boolean` to the Expo plugin? - Or a way to bypass device info checks for OAuth flows (since they originate from external browsers)? ## Additional Context ### OAuth Flow Diagram ``` Expo App (creativebook://) ↓ (1) authClient.signIn.social({ provider: "google" }) ↓ Next.js Server (http://192.168.x.x:3000) ↓ (2) Generates OAuth URL with callback ↓ External Browser (Chrome/Safari) ↓ (3) User authenticates with Google ↓ (4) Google redirects to callback URL ↓ Next.js Server (http://192.168.x.x:3000/api/auth/callback/google) ❌ (5) ERROR: device id and device name required ``` ### Why Network IP? We use network IP instead of localhost because: - OAuth callbacks come from external browser (Chrome/Safari) - External browser cannot reach `localhost:3000` - Network IP (`192.168.x.x:3000`) is accessible from the device's browser ### Expo Plugin Context The Expo plugin documentation mentions it's for "mobile device management" but doesn't clarify: - How device info should be provided during OAuth flows - Whether OAuth flows should bypass device info requirements - How the plugin interacts with the OAuth proxy ## Workaround Attempted We tried various configurations but none resolved the issue: 1. **Using `baseURL` as network IP** (current approach) 2. **Removing explicit `redirectURI`** (didn't help) 3. **Adding `trustedOrigins` with Expo schemes** (still fails) ## Related Issues/Discussions This appears related to the broader challenge of using Better Auth OAuth with Expo apps in development, where: - Development servers use dynamic network IPs - OAuth callbacks must be accessible from external browsers - Mobile apps need different handling than web apps ## Environment Details **Server logs:** ``` 🔐 Better Auth OAuth Config: baseUrl: http://localhost:3000 productionUrl: http://192.168.x.x:3000 ✅ OAuth callbacks will redirect to: http://192.168.x.x:3000/api/auth/callback/* ``` **Expo logs:** ``` 📱 Expo Base URL Config: debuggerHost: 192.168.x.x:8081 hostIp: 192.168.x.x baseUrl: http://192.168.x.x:3000 ``` **OAuth authorization URL (works correctly):** ``` https://accounts.google.com/o/oauth2/auth? response_type=code &client_id=... &redirect_uri=http://192.168.x.x:3000/api/auth/callback/google ... ``` ## Proposed Solutions 1. **Option A:** Add `requireDeviceInfo: boolean` to Expo plugin options ```typescript expo({ requireDeviceInfo: false, // Disable for OAuth flows }) ``` 2. **Option B:** Auto-detect OAuth flows and skip device info - Expo plugin automatically bypasses device checks when request comes from OAuth proxy 3. **Option C:** Allow device info to be optional in specific contexts - Document how to provide device info from Expo client during OAuth 4. **Option D:** Documentation clarification - If this is a configuration issue, document the correct pattern for Expo + OAuth ## Request Could you please clarify: - Is this expected behavior or a bug? - What's the recommended pattern for Expo + OAuth in development? - Should device info be provided, and if so, how? --- **Full configuration details:** https://github.com/issam-seghir/creative-book-mobile/blob/main/docs/BETTER_AUTH_OAUTH_ISSUE.md
GiteaMirror added the locked label 2026-04-15 18:46:33 -05:00
Author
Owner

@dosubot[bot] commented on GitHub (Feb 4, 2026):

This error is not enforced by the Better Auth Expo plugin or its OAuth routes—there is no code in those areas that requires device id or device name for OAuth flows. The Expo plugin handles OAuth state/cookie proxying and origin headers, but does not interact with device metadata or expose config options for it. The social sign-in endpoint also does not check for device info during OAuth flows (source).

This means the error is likely coming from another part of your stack, such as a device-authorization plugin, custom middleware, or an upstream proxy that is enforcing device info requirements on private API endpoints—even for OAuth callbacks. Double-check your server config for any such plugins or middleware and try disabling them for OAuth routes.

For Expo + OAuth in development, the recommended pattern is to use a public tunnel (like ngrok) for your backend so you can register a valid HTTPS redirect URI with Google (Google does not accept private IPs as redirect URIs), ensure your Expo app scheme is in trustedOrigins, and confirm the /expo-authorization-proxy endpoint is available on your server (source).

You do not need to provide device info for OAuth flows with Better Auth + Expo. If you still see this error after checking for extra middleware, try switching to a public tunnel for your backend and re-testing the flow.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

<!-- gh-comment-id:3847501153 --> @dosubot[bot] commented on GitHub (Feb 4, 2026): <!-- Answer --> This error is not enforced by the Better Auth Expo plugin or its OAuth routes—there is no code in those areas that requires device id or device name for OAuth flows. The Expo plugin handles OAuth state/cookie proxying and origin headers, but does not interact with device metadata or expose config options for it. The social sign-in endpoint also does not check for device info during OAuth flows [(source)](https://github.com/better-auth/better-auth/blob/583f12fb7db38681ee882d219ce3895faa4a6976/packages/expo/src/index.ts#L17-L87). This means the error is likely coming from another part of your stack, such as a device-authorization plugin, custom middleware, or an upstream proxy that is enforcing device info requirements on private API endpoints—even for OAuth callbacks. Double-check your server config for any such plugins or middleware and try disabling them for OAuth routes. For Expo + OAuth in development, the recommended pattern is to use a public tunnel (like ngrok) for your backend so you can register a valid HTTPS redirect URI with Google (Google does not accept private IPs as redirect URIs), ensure your Expo app scheme is in `trustedOrigins`, and confirm the `/expo-authorization-proxy` endpoint is available on your server [(source)](https://github.com/better-auth/better-auth/issues/4070). You do not need to provide device info for OAuth flows with Better Auth + Expo. If you still see this error after checking for extra middleware, try switching to a public tunnel for your backend and re-testing the flow. <!-- Dosu Comment Footer --> *To reply, just mention [@dosu](https://go.dosu.dev/dosubot).* --- <sup>How did I do? [Good](https://app.dosu.dev/response-feedback/a80fda15-b8ee-4812-9a72-c8ffdecb745e?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/a80fda15-b8ee-4812-9a72-c8ffdecb745e?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/a80fda15-b8ee-4812-9a72-c8ffdecb745e?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/a80fda15-b8ee-4812-9a72-c8ffdecb745e?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/a80fda15-b8ee-4812-9a72-c8ffdecb745e?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/a80fda15-b8ee-4812-9a72-c8ffdecb745e?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/a80fda15-b8ee-4812-9a72-c8ffdecb745e?feedback_type=other)</sup> [![Chat with Dosu](https://dosu.dev/dosu-chat-badge.svg)](https://app.dosu.dev/cdda13d9-dd27-4d31-b09a-5d8bec92de21/ask?utm_source=github)&nbsp;[![Open in Cursor](https://dosu.dev/dosu-in-cursor.svg)](https://cursor.com/link/prompt?text=This%20error%20is%20not%20enforced%20by%20the%20Better%20Auth%20Expo%20plugin%20or%20its%20OAuth%20routes%E2%80%94there%20is%20no%20code%20in%20those%20areas%20that%20requires%20device%20id%20or%20device%20name%20for%20OAuth%20flows.%20The%20Expo%20plugin%20handles%20OAuth%20state/cookie%20proxying%20and%20origin%20headers%2C%20but%20does%20not%20interact%20with%20device%20metadata%20or%20expose%20config%20options%20for%20it.%20The%20social%20sign-in%20endpoint%20also%20does%20not%20check%20for%20device%20info%20during%20OAuth%20flows%20%5B%28source%29%5D%28https%3A//github.com/better-auth/better-auth/blob/583f12fb7db38681ee882d219ce3895faa4a6976/packages/expo/src/index.ts%23L17-L87%29.%0A%0AThis%20means%20the%20error%20is%20likely%20coming%20from%20another%20part%20of%20your%20stack%2C%20such%20as%20a%20device-authorization%20plugin%2C%20custom%20middleware%2C%20or%20an%20upstream%20proxy%20that%20is%20enforcing%20device%20info%20requirements%20on%20private%20API%20endpoints%E2%80%94even%20for%20OAuth%20callbacks.%20Double-check%20your%20server%20config%20for%20any%20such%20plugins%20or%20middleware%20and%20try%20disabling%20them%20for%20OAuth%20routes.%0A%0AFor%20Expo%20%2B%20OAuth%20in%20development%2C%20the%20recommended%20pattern%20is%20to%20use%20a%20public%20tunnel%20%28like%20ngrok%29%20for%20your%20backend%20so%20you%20can%20register%20a%20valid%20HTTPS%20redirect%20URI%20with%20Google%20%28Google%20does%20not%20accept%20private%20IPs%20as%20redirect%20URIs%29%2C%20ensure%20your%20Expo%20app%20scheme%20is%20in%20%60trustedOrigins%60%2C%20and%20confirm%20the%20%60/expo-authorization-proxy%60%20endpoint%20is%20available%20on%20your%20server%20%5B%28source%29%5D%28https%3A//github.com/better-auth/better-auth/issues/4070%29.%0A%0AYou%20do%20not%20need%20to%20provide%20device%20info%20for%20OAuth%20flows%20with%20Better%20Auth%20%2B%20Expo.%20If%20you%20still%20see%20this%20error%20after%20checking%20for%20extra%20middleware%2C%20try%20switching%20to%20a%20public%20tunnel%20for%20your%20backend%20and%20re-testing%20the%20flow.)&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/better-auth/better-auth/issues/7791)
Author
Owner

@bytaesu commented on GitHub (Feb 4, 2026):

Hi @issam-seghir,

I'll reproduce this today together with https://github.com/better-auth/better-auth/issues/7782

<!-- gh-comment-id:3848022951 --> @bytaesu commented on GitHub (Feb 4, 2026): Hi @issam-seghir, I'll reproduce this today together with https://github.com/better-auth/better-auth/issues/7782
Author
Owner

@bytaesu commented on GitHub (Feb 4, 2026):

Note

Image

https://stackoverflow.com/questions/27045616/google-oauth-device-id-and-device-name-are-required-for-private-ip

This is a policy from Google OAuth. We can't handle it, and the redirect URI likely wasn't registered with Google in the first place. And.. productionUrl refers to your actual production URL. You may consider using Cloudflare Tunnel or something 🤔

<!-- gh-comment-id:3850041142 --> @bytaesu commented on GitHub (Feb 4, 2026): > [!NOTE] > <img width="511" height="242" alt="Image" src="https://github.com/user-attachments/assets/65428b37-89d1-4f28-b189-87df84491c3c" /> > > https://stackoverflow.com/questions/27045616/google-oauth-device-id-and-device-name-are-required-for-private-ip This is a policy from Google OAuth. We can't handle it, and the redirect URI likely wasn't registered with Google in the first place. And.. productionUrl refers to your actual production URL. You may consider using Cloudflare Tunnel or something 🤔
Author
Owner

@issam-seghir commented on GitHub (Feb 4, 2026):

@bytaesu I'm using t3 turbo stack and following this guide using Oauth proxy plugin, they added an option to use a private ip address

<!-- gh-comment-id:3850263613 --> @issam-seghir commented on GitHub (Feb 4, 2026): @bytaesu I'm using t3 turbo stack and following this [guide](https://github.com/t3-oss/create-t3-turbo#add-your-local-ip-to-your-oauth-provider) using Oauth proxy plugin, they added an option to use a private ip address
Author
Owner

@bytaesu commented on GitHub (Feb 7, 2026):

@issam-seghir, While this might be possible with some social providers, Google does not allow it due to its policies, so it’s outside of our control 🙏

<!-- gh-comment-id:3863857338 --> @bytaesu commented on GitHub (Feb 7, 2026): @issam-seghir, While this might be possible with some social providers, Google does not allow it due to its policies, so it’s outside of our control 🙏
Author
Owner

@abrahamkbbe commented on GitHub (Mar 7, 2026):

import { useState } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input";

export default function OnlineStoreApp() { const [user, setUser] = useState(null); const [email, setEmail] = useState(""); const [cart, setCart] = useState([]); const [products, setProducts] = useState([ { id: 1, name: "T-Shirt", price: 20 }, { id: 2, name: "Hoodie", price: 40 }, { id: 3, name: "Cap", price: 15 }, { id: 4, name: "Sneakers", price: 80 } ]);

const [newProductName, setNewProductName] = useState(""); const [newProductPrice, setNewProductPrice] = useState("");

const login = () => { if (email) setUser({ email }); };

const logout = () => { setUser(null); setCart([]); };

const addToCart = (product) => { setCart([...cart, product]); };

const addProduct = () => { if (!newProductName || !newProductPrice) return;

const newProduct = {
id: Date.now(),
name: newProductName,
price: Number(newProductPrice)
};

setProducts([...products, newProduct]);
setNewProductName("");
setNewProductPrice("");

};

const total = cart.reduce((sum, item) => sum + item.price, 0);

const checkout = () => { alert("Stripe payment integration goes here 💳"); };

if (!user) { return (

Login

<Input placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} /> Login
); }

return (

Online Store

Logout

Add Product (Admin)

setNewProductName(e.target.value)} /> setNewProductPrice(e.target.value)} /> Add Product
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
  {products.map((product) => (
    <Card key={product.id} className="rounded-2xl shadow">
      <CardContent className="p-4 flex flex-col gap-3">
        <h2 className="text-xl font-semibold">{product.name}</h2>
        <p className="text-gray-600">${product.price}</p>
        <Button onClick={() => addToCart(product)}>
          Add to Cart
        </Button>
      </CardContent>
    </Card>
  ))}
</div>

Cart

{cart.length === 0 && <p>No items yet</p>}

{cart.map((item, i) => (
  <div key={i} className="flex justify-between py-1">
    <span>{item.name}</span>
    <span>${item.price}</span>
  </div>
))}

<div className="border-t mt-3 pt-3 font-bold">Total: ${total}</div>

<Button className="w-full mt-4" onClick={checkout}>
  Checkout
</Button>

); }

<!-- gh-comment-id:4016946179 --> @abrahamkbbe commented on GitHub (Mar 7, 2026): import { useState } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; export default function OnlineStoreApp() { const [user, setUser] = useState(null); const [email, setEmail] = useState(""); const [cart, setCart] = useState([]); const [products, setProducts] = useState([ { id: 1, name: "T-Shirt", price: 20 }, { id: 2, name: "Hoodie", price: 40 }, { id: 3, name: "Cap", price: 15 }, { id: 4, name: "Sneakers", price: 80 } ]); const [newProductName, setNewProductName] = useState(""); const [newProductPrice, setNewProductPrice] = useState(""); const login = () => { if (email) setUser({ email }); }; const logout = () => { setUser(null); setCart([]); }; const addToCart = (product) => { setCart([...cart, product]); }; const addProduct = () => { if (!newProductName || !newProductPrice) return; const newProduct = { id: Date.now(), name: newProductName, price: Number(newProductPrice) }; setProducts([...products, newProduct]); setNewProductName(""); setNewProductPrice(""); }; const total = cart.reduce((sum, item) => sum + item.price, 0); const checkout = () => { alert("Stripe payment integration goes here 💳"); }; if (!user) { return ( <div className="h-screen flex items-center justify-center"> <Card className="p-6 w-80"> <CardContent className="flex flex-col gap-4"> <h2 className="text-xl font-bold">Login</h2> <Input placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} /> <Button onClick={login}>Login</Button> </CardContent> </Card> </div> ); } return ( <div className="p-6 grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="md:col-span-2 flex flex-col gap-6"> <div className="flex justify-between items-center"> <h1 className="text-2xl font-bold">Online Store</h1> <Button onClick={logout}>Logout</Button> </div> <Card className="rounded-2xl"> <CardContent className="p-4 flex flex-col gap-3"> <h2 className="font-semibold">Add Product (Admin)</h2> <Input placeholder="Product name" value={newProductName} onChange={(e) => setNewProductName(e.target.value)} /> <Input placeholder="Price" type="number" value={newProductPrice} onChange={(e) => setNewProductPrice(e.target.value)} /> <Button onClick={addProduct}>Add Product</Button> </CardContent> </Card> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> {products.map((product) => ( <Card key={product.id} className="rounded-2xl shadow"> <CardContent className="p-4 flex flex-col gap-3"> <h2 className="text-xl font-semibold">{product.name}</h2> <p className="text-gray-600">${product.price}</p> <Button onClick={() => addToCart(product)}> Add to Cart </Button> </CardContent> </Card> ))} </div> </div> <div className="bg-gray-50 p-4 rounded-2xl shadow h-fit"> <h2 className="text-xl font-bold mb-4">Cart</h2> {cart.length === 0 && <p>No items yet</p>} {cart.map((item, i) => ( <div key={i} className="flex justify-between py-1"> <span>{item.name}</span> <span>${item.price}</span> </div> ))} <div className="border-t mt-3 pt-3 font-bold">Total: ${total}</div> <Button className="w-full mt-4" onClick={checkout}> Checkout </Button> </div> </div> ); }
Author
Owner

@abrahamkbbe commented on GitHub (Mar 28, 2026):

Bana sanal market için Android app yaz

<!-- gh-comment-id:4148542056 --> @abrahamkbbe commented on GitHub (Mar 28, 2026): Bana sanal market için Android app yaz
Author
Owner

@github-actions[bot] commented on GitHub (Apr 5, 2026):

This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.

<!-- gh-comment-id:4187977897 --> @github-actions[bot] commented on GitHub (Apr 5, 2026): This issue has been locked as it was closed more than 7 days ago. If you're experiencing a similar problem or you have additional context, please open a new issue and reference this one.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#19537