diff --git a/src/components/organisms/Marketplace/MarketplaceSessionConnectDialog.test.tsx b/src/components/organisms/Marketplace/MarketplaceSessionConnectDialog.test.tsx
index 0d53e88ff9..7602efaa51 100644
--- a/src/components/organisms/Marketplace/MarketplaceSessionConnectDialog.test.tsx
+++ b/src/components/organisms/Marketplace/MarketplaceSessionConnectDialog.test.tsx
@@ -78,6 +78,9 @@ describe('MarketplaceSessionConnectDialog', () => {
expect(screen.getByLabelText('Copy authorization link')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /open in pubky ring/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /copy link/i })).toBeInTheDocument();
+ expect(screen.getByText('Sign in to Pubky Shop.')).toBeInTheDocument();
+ expect(screen.queryByText(/permission list/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/compare it before approving/i)).not.toBeInTheDocument();
});
it('uses reconnect copy only when the hook selected grant reconnect', () => {
diff --git a/src/components/organisms/Marketplace/MarketplaceSessionConnectDialog.tsx b/src/components/organisms/Marketplace/MarketplaceSessionConnectDialog.tsx
index 8c92b061a4..f34674f73d 100644
--- a/src/components/organisms/Marketplace/MarketplaceSessionConnectDialog.tsx
+++ b/src/components/organisms/Marketplace/MarketplaceSessionConnectDialog.tsx
@@ -5,9 +5,9 @@ import { Copy, KeyRound, Loader2, RefreshCw, Smartphone } from 'lucide-react';
import { Button } from '@/atoms/Button/Button';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/atoms/Dialog/Dialog';
import { Typography } from '@/atoms/Typography/Typography';
-import { CAPABILITIES } from '@/config/app';
import { useMarketplaceSessionConnect } from '@/hooks/useMarketplaceSessionConnect/useMarketplaceSessionConnect';
import { Logger } from '@/libs/logger/logger';
+import { getMarketplaceGrantFlowEnabled } from '@/libs/runtime-config/runtime-config';
import { QrCodeSlot } from '@/molecules/QrCodeSlot/QrCodeSlot';
import { toast } from '@/molecules/Toaster/use-toast';
@@ -32,6 +32,7 @@ export function MarketplaceSessionConnectDialog({
autoOpen?: boolean;
}) {
const [open, setOpen] = useState(false);
+ const grantFlowEnabled = getMarketplaceGrantFlowEnabled();
const session = useMarketplaceSessionConnect({
onConnected: () => {
toast({
@@ -90,27 +91,10 @@ export function MarketplaceSessionConnectDialog({
{requestsGrantReconnect
? 'Approve with Bitkit or Pubky Ring to reconnect the marketplace session for the identity already signed in to Shop. Nothing is charged until you pay.'
- : requestsFullGrant
- ? 'Approving with Pubky Ring signs you in to Shop with the full permission list and lets this marketplace place orders, bids, and offers as you. Nothing is charged until you pay.'
- : 'Approving with Pubky Ring lets this marketplace place orders, bids, and offers as you. Nothing is charged until you pay. The approval stays on this device across tabs and restarts until it expires or you sign out.'}
+ : requestsFullGrant && !grantFlowEnabled
+ ? 'Sign in to Pubky Shop.'
+ : 'Approve purchases for this device.'}
-
- {requestsGrantReconnect
- ? 'Your signer will show an empty permission list. This approval proves identity to the marketplace service and does not change homeserver access.'
- : requestsFullGrant
- ? 'Ring will show the full permission list — that is correct. This is the first Shop-scoped approval; it was not covered by signing in on pubky.app.'
- : 'Ring will show an empty permission list — that is correct. This approval only proves your identity to the marketplace service; it grants no read or write access to anything on your homeserver.'}
-
- {requestsFullGrant && !requestsGrantReconnect && (
-
-
- {CAPABILITIES}
-
-
- Pubky Ring will show this exact permission list — compare it before approving.
-
-
- )}
{['error', 'mismatch', 'expired', 'cancelled'].includes(session.status) ? (
diff --git a/src/components/organisms/SignIn/SignIn.test.tsx b/src/components/organisms/SignIn/SignIn.test.tsx
index 6a799d7ae6..3c612e20a3 100644
--- a/src/components/organisms/SignIn/SignIn.test.tsx
+++ b/src/components/organisms/SignIn/SignIn.test.tsx
@@ -384,12 +384,13 @@ describe('SignInContent', () => {
expect(button).toBeDisabled();
});
- it('renders the exact CAPABILITIES string beside the QR', async () => {
+ it('does not print requested capability paths beside the QR', async () => {
const { CAPABILITIES } = await import('@/config/app');
await act(async () => {
render(
);
});
- expect(screen.getByText(CAPABILITIES)).toBeInTheDocument();
+ expect(screen.queryByText(CAPABILITIES)).not.toBeInTheDocument();
+ expect(screen.queryByText(/compare it before approving/i)).not.toBeInTheDocument();
});
it('renders content cards with column layout', async () => {
diff --git a/src/components/organisms/SignIn/SignIn.test.tsx.snap b/src/components/organisms/SignIn/SignIn.test.tsx.snap
index 096b1e267b..2b96408bc7 100644
--- a/src/components/organisms/SignIn/SignIn.test.tsx.snap
+++ b/src/components/organisms/SignIn/SignIn.test.tsx.snap
@@ -73,22 +73,6 @@ exports[`SignInContent - Snapshots > matches snapshot for the QR sign-in layout
width="48"
/>
-
-
- /pub/pubky.app/:rw,/pub/paykit/:rw,/priv/pubky.app/:rw
-
-
- Pubky Ring will show this exact permission list — compare it before approving.
-
-
{
activeQrHasHoverEffect
/>
-
-
- {CAPABILITIES}
-
-
- Pubky Ring will show this exact permission list — compare it before approving.
-
-
diff --git a/src/components/templates/Marketplace/MarketplaceCart.test.tsx b/src/components/templates/Marketplace/MarketplaceCart.test.tsx
index 0b5ed58014..d56e5f7af3 100644
--- a/src/components/templates/Marketplace/MarketplaceCart.test.tsx
+++ b/src/components/templates/Marketplace/MarketplaceCart.test.tsx
@@ -250,30 +250,33 @@ describe('MarketplaceCart', () => {
});
it.each(['transaction-service', 'locks-paykit', 'unavailable'] as const)(
- 'shows the fail-closed real-money notice in %s mode and the interim address copy',
+ 'shows muted seller-direct helper in %s production checkout, not an amber money warning',
(adapterMode) => {
seededCart();
view.adapterMode = adapterMode;
view.hasMarketplaceSession = true;
+ view.deployEnv = 'production';
render(
);
- expect(screen.getAllByRole('note')).toHaveLength(1);
- expect(screen.getByRole('note')).toHaveTextContent(
- 'Real money. Payments are final and go directly to the seller.',
- );
+ expect(screen.queryByRole('note')).not.toBeInTheDocument();
+ expect(
+ screen.queryByText('Real money. Payments are final and go directly to the seller.'),
+ ).not.toBeInTheDocument();
+ expect(screen.getByText('Paid directly to the seller.')).toBeInTheDocument();
expect(screen.getByText(MARKETPLACE_DELIVERY_ADDRESS_DISCLOSURE)).toBeInTheDocument();
},
);
- it('fails closed to the real-money notice for an unknown deploy environment', () => {
+ it('does not show a production money warning when the deploy environment is unknown', () => {
seededCart();
view.adapterMode = 'sandbox';
view.deployEnv = undefined;
render(
);
- expect(screen.getByRole('note')).toHaveTextContent('Real money. Payments are final and go directly to the seller.');
+ expect(screen.queryByRole('note')).not.toBeInTheDocument();
+ expect(screen.queryByText(/Real money/)).not.toBeInTheDocument();
expect(screen.queryByText(/Staging environment/)).not.toBeInTheDocument();
});
@@ -286,6 +289,7 @@ describe('MarketplaceCart', () => {
expect(screen.getByRole('note')).toHaveTextContent('Staging environment — test rails, no real funds move');
expect(screen.queryByText('Real money. Payments are final and go directly to the seller.')).not.toBeInTheDocument();
+ expect(screen.queryByText('Paid directly to the seller.')).not.toBeInTheDocument();
});
it.each([false, true])('renders truthful address copy exactly once with saved addresses=%s', (hasSavedAddress) => {
diff --git a/src/components/templates/Marketplace/MarketplaceCart.tsx b/src/components/templates/Marketplace/MarketplaceCart.tsx
index 969956414f..0d43a6d9f6 100644
--- a/src/components/templates/Marketplace/MarketplaceCart.tsx
+++ b/src/components/templates/Marketplace/MarketplaceCart.tsx
@@ -506,7 +506,7 @@ export function MarketplaceCart() {
This places {checkout.orderCount} orders — one per seller and delivery method.
)}
- {isStaging ? (
+ {isStaging && (
Staging environment — test rails, no real funds move
- ) : (
-
- Real money. Payments are final and go directly to the seller.
-
)}
{isSandbox ? 'Place sandbox order' : 'Place order'}
+ {!isStaging && !isSandbox && (
+
+ Paid directly to the seller.
+
+ )}
{!canPlaceOrder && !approvalNeeded && (
{checkout.hasFulfillmentConflict
diff --git a/src/components/templates/Marketplace/MarketplaceListing.test.tsx b/src/components/templates/Marketplace/MarketplaceListing.test.tsx
index c7469a10b3..b58e48259a 100644
--- a/src/components/templates/Marketplace/MarketplaceListing.test.tsx
+++ b/src/components/templates/Marketplace/MarketplaceListing.test.tsx
@@ -382,10 +382,10 @@ describe('MarketplaceListing', () => {
expect(screen.getByRole('button', { name: 'Approve in Pubky Ring' })).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Approve in Pubky Ring' }));
- expect(screen.getByText(CAPABILITIES)).toBeInTheDocument();
- expect(
- screen.getByText(/this is the first Shop-scoped approval; it was not covered by signing in on pubky.app/i),
- ).toBeInTheDocument();
+ expect(screen.getByText('Sign in to Pubky Shop.')).toBeInTheDocument();
+ expect(screen.queryByText(/permission list/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(CAPABILITIES)).not.toBeInTheDocument();
+ expect(screen.queryByText(/first Shop-scoped approval/i)).not.toBeInTheDocument();
});
it('labels an ended auction Auction ended and does not blame the seller for checkout unavailability', () => {
@@ -436,7 +436,8 @@ describe('MarketplaceListing', () => {
expect(screen.getByRole('button', { name: 'Approve in Pubky Ring' })).toBeInTheDocument();
await user.click(screen.getByRole('button', { name: 'Approve in Pubky Ring' }));
- expect(screen.getByText(/Ring will show an empty permission list/i)).toBeInTheDocument();
+ expect(screen.getByText('Approve purchases for this device.')).toBeInTheDocument();
+ expect(screen.queryByText(/permission list/i)).not.toBeInTheDocument();
expect(screen.queryByText(CAPABILITIES)).not.toBeInTheDocument();
});
});
diff --git a/src/components/templates/Marketplace/MarketplacePaymentSettings.test.tsx b/src/components/templates/Marketplace/MarketplacePaymentSettings.test.tsx
index 849660a892..a23fac219f 100644
--- a/src/components/templates/Marketplace/MarketplacePaymentSettings.test.tsx
+++ b/src/components/templates/Marketplace/MarketplacePaymentSettings.test.tsx
@@ -129,13 +129,15 @@ function setPaykitIframeSource(iframe: HTMLIFrameElement): WindowProxy {
}
describe('MarketplacePaymentSettings', () => {
- it('leads with the seller-direct promise', async () => {
+ it('leads with how-you-get-paid setup copy, not a funds warning', async () => {
await renderSettings();
expect(screen.getByRole('heading', { name: 'How you get paid' })).toBeInTheDocument();
- expect(
- screen.getByText('Every method pays the seller directly — this marketplace never holds funds.'),
- ).toBeInTheDocument();
+ expect(screen.getByText('Set up the methods buyers can use at checkout.')).toBeInTheDocument();
+ expect(screen.queryByText(/pays the seller directly/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/never holds funds/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/pre-production/i)).not.toBeInTheDocument();
+ expect(screen.queryByText(/this prototype/i)).not.toBeInTheDocument();
});
it('renders the three method cards in buyer-familiar order', async () => {
@@ -255,6 +257,19 @@ describe('MarketplacePaymentSettings', () => {
expect(mockedController.putMyPaymentConfig).not.toHaveBeenCalled();
});
+ it('tells Ring-signed-up sellers to create a Shop identity in Bitkit', async () => {
+ await renderSettings();
+
+ const helper =
+ 'Your Shop identity must live in Bitkit. Signed up with Pubky Ring? Create a new Shop account by scanning the sign-up QR with Bitkit — Ring import is coming to Bitkit.';
+ expect(screen.getByText(helper)).toBeInTheDocument();
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole('button', { name: /Open Bitkit setup/ }));
+ expect(screen.getAllByText(helper)).toHaveLength(2);
+ expect(screen.getByText(/Bitkit 2.5 or newer is required/)).toBeInTheDocument();
+ });
+
it('validates the Bitkit setup callback', async () => {
render( );
expect(screen.getByRole('heading', { name: 'PayPal' })).toBeInTheDocument();
diff --git a/src/components/templates/Marketplace/MarketplacePaymentSettings.tsx b/src/components/templates/Marketplace/MarketplacePaymentSettings.tsx
index 8225f03b2e..cc23189abb 100644
--- a/src/components/templates/Marketplace/MarketplacePaymentSettings.tsx
+++ b/src/components/templates/Marketplace/MarketplacePaymentSettings.tsx
@@ -107,7 +107,7 @@ export function MarketplacePaymentSettings() {
How you get paid
- Every method pays the seller directly — this marketplace never holds funds.
+ Set up the methods buyers can use at checkout.
@@ -212,11 +212,6 @@ export function MarketplacePaymentSettings() {
-
-
- Locks and Paykit Server are pre-production. Do not use this prototype to protect valuable content or real
- funds without an independent security and operational review.
-
);
diff --git a/src/libs/commerce/checkout-hold.test.ts b/src/libs/commerce/checkout-hold.test.ts
index 67e0777acf..4189f97fe1 100644
--- a/src/libs/commerce/checkout-hold.test.ts
+++ b/src/libs/commerce/checkout-hold.test.ts
@@ -20,11 +20,12 @@ describe('checkout-hold copy', () => {
it('renders unbound and bound holder copy with the deadline', () => {
expect(holderUnboundCopy('2026-08-20T21:15:00.000Z')).toBe(
- 'This item is held for you until Aug 20, 2026, 9:15 PM UTC. Choose how to pay. Every method pays the seller directly.',
+ 'The item is held for you until Aug 20, 2026, 9:15 PM UTC.',
);
expect(holderBoundCopy('2026-08-20T21:15:00.000Z')).toBe(
'Pay by Aug 20, 2026, 9:15 PM UTC. If the window ends, the item restocks.',
);
+ expect(holderUnboundCopy(null)).toBe('The item is held for you once a payment starts.');
});
it('detects late completion from paid plus leftover elapsed reason', () => {
diff --git a/src/libs/commerce/checkout-hold.ts b/src/libs/commerce/checkout-hold.ts
index 5a6614b19b..d5478e80fb 100644
--- a/src/libs/commerce/checkout-hold.ts
+++ b/src/libs/commerce/checkout-hold.ts
@@ -35,9 +35,7 @@ export function formatHoldDeadline(holdExpiresAt: string | null | undefined): st
export function holderUnboundCopy(holdExpiresAt: string | null | undefined): string {
const deadline = formatHoldDeadline(holdExpiresAt);
- return deadline
- ? `This item is held for you until ${deadline}. Choose how to pay. Every method pays the seller directly.`
- : 'This item is held for you. Choose how to pay. Every method pays the seller directly.';
+ return deadline ? `The item is held for you until ${deadline}.` : 'The item is held for you once a payment starts.';
}
export function holderBoundCopy(holdExpiresAt: string | null | undefined): string {
diff --git a/src/test/vrt/marketplace/MarketplaceCart.vrt.test.tsx b/src/test/vrt/marketplace/MarketplaceCart.vrt.test.tsx
index e07d8496e3..3b5c74aa99 100644
--- a/src/test/vrt/marketplace/MarketplaceCart.vrt.test.tsx
+++ b/src/test/vrt/marketplace/MarketplaceCart.vrt.test.tsx
@@ -166,10 +166,10 @@ vi.mock('@/config/commerce', async (importOriginal) => {
return { ...actual, getCommerceAdapterMode: () => view.adapterMode };
});
-// The checkout money notice is gated on the deploy environment, not the
+// The checkout staging amber is gated on the deploy environment, not the
// adapter mode. Each scene names its environment explicitly: staging (test
// rails, no real funds) is the default; the locks-paykit scene runs on
-// production, where real payment rails are live.
+// production, where the muted helper is "Paid directly to the seller."
vi.mock('@/libs/runtime-config/runtime-config', async (importOriginal) => {
const actual = await importOriginal