Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/next/paykit-ring-identity-helper.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Seller Studio Paykit setup Step 2 now tells Ring-signed-up sellers to create a Shop identity in Bitkit. Ring import is not in Bitkit yet.
1 change: 1 addition & 0 deletions changelog.d/next/remove-maturity-warnings.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Removed unsolicited production maturity and real-money warning banners from Shop checkout, orders, payment settings, and auth QR dialogs. Staging still shows the test-rails notice; checkout keeps a muted seller-direct helper; awaiting payment keeps the hold sentence.
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ type PaykitSetupStatus = 'idle' | 'error' | 'mismatch' | 'verifying' | 'timeout'
const PAYKIT_SETUP_TIMEOUT_MS = 6 * 60 * 1_000;
const PAYKIT_SETUP_EXPLANATION =
'Scan the code with Bitkit, or open this page on your phone and tap Open in Bitkit. Bitkit 2.5 or newer is required.';
const PAYKIT_RING_IDENTITY_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.';

function createPaykitSetupState(): string {
const bytes = new Uint8Array(16);
Expand Down Expand Up @@ -477,6 +479,9 @@ export function MarketplaceGetPaidSettings({ locksConnect, onSaved }: Marketplac
Open the setup in Bitkit and approve it there. Payments settle to your own bitcoin wallet — your spending
keys never leave it.
</Typography>
<Typography as="p" className="mt-2 text-sm text-muted-foreground">
{PAYKIT_RING_IDENTITY_HELPER}
</Typography>
{payments.accountClaimed === true && (
<Typography as="p" className="mt-2 flex items-center gap-2 text-sm text-brand">
<CheckCircle2 className="size-4" />
Expand Down Expand Up @@ -522,7 +527,7 @@ export function MarketplaceGetPaidSettings({ locksConnect, onSaved }: Marketplac
</CollapsibleTrigger>
<CollapsibleContent className="grid gap-3 rounded-xl border p-4 text-sm text-muted-foreground data-[state=closed]:hidden">
<Typography as="p" className="text-sm text-muted-foreground">
Pubky Ring displays the exact creator capability grant. No identity secret enters Pubky App.
No identity secret enters this app.
</Typography>
<Typography as="p" className="text-sm text-muted-foreground">
Bitkit sends a watch-only BIP84 account claim directly to Paykit Server. Spending keys remain in the
Expand Down Expand Up @@ -584,6 +589,9 @@ export function MarketplaceGetPaidSettings({ locksConnect, onSaved }: Marketplac
<Typography as="p" className="text-sm text-muted-foreground">
{PAYKIT_SETUP_EXPLANATION}
</Typography>
<Typography as="p" className="text-sm text-muted-foreground">
{PAYKIT_RING_IDENTITY_HELPER}
</Typography>
{paykitSetupUrl && (
<iframe
ref={paykitIframeRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { Button } from '@/atoms/Button/Button';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/atoms/Dialog/Dialog';
import { Typography } from '@/atoms/Typography/Typography';
import { useMarketplaceMessagingEnable } from '@/hooks/useMarketplaceMessagingEnable/useMarketplaceMessagingEnable';
import { PAYKIT_MESSAGING_CAPABILITY } from '@/libs/commerce/messaging-contracts';
import { Logger } from '@/libs/logger/logger';
import { QrCodeSlot } from '@/molecules/QrCodeSlot/QrCodeSlot';
import { toast } from '@/molecules/Toaster/use-toast';
Expand Down Expand Up @@ -65,15 +64,7 @@ export function MarketplaceMessagingEnablePanel({
return (
<div className="grid gap-4">
<Typography as="p" className="text-sm text-muted-foreground">
Messaging normally needs no extra approval: a sign-in made with the current grant already covers messaging and
connects automatically — if that were your case, you would never see this step. You are seeing it because this
sign-in predates the combined grant (its session has no messaging scope) or the homeserver no longer accepts its
session. Approving once with your signer (Pubky Ring) grants this app a homeserver session scoped to{' '}
<code className="rounded bg-secondary px-1 py-0.5 text-xs">{PAYKIT_MESSAGING_CAPABILITY}</code> — the Paykit
tree where encrypted-message data lives, plus the app&apos;s own storage scope (the homeserver keeps one session
per browser, so this approval also carries your normal posting and publishing access). Your identity key never
enters this browser; message encryption uses a separate key generated and kept on this device. After this
one-time approval, messaging resumes automatically on this device until the session expires or you sign out.
{reconnect ? 'Reconnect encrypted messaging for this device.' : 'Approve encrypted messaging for this device.'}
</Typography>

{enable.status === 'error' ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ describe('MarketplacePaymentStatusCard', () => {
});

it.each(['transaction-service', 'locks-paykit', 'unavailable'] as const)(
'shows the non-dismissible real-money notice in %s mode',
'does not show a production money warning in %s mode',
(adapterMode) => {
render(
<MarketplacePaymentStatusCard
Expand All @@ -146,13 +146,14 @@ describe('MarketplacePaymentStatusCard', () => {
/>,
);

const notice = screen.getByRole('note');
expect(notice).toHaveTextContent('Real money. Payments are final and go directly to the seller.');
expect(notice.querySelector('button')).not.toBeInTheDocument();
expect(screen.queryByRole('note')).not.toBeInTheDocument();
expect(
screen.queryByText('Real money. Payments are final and go directly to the seller.'),
).not.toBeInTheDocument();
},
);

it('shows the staging notice instead of the real-money notice on a staging deploy', () => {
it('shows the staging notice on a staging deploy', () => {
runtime.deployEnv = 'staging';
render(
<MarketplacePaymentStatusCard
Expand All @@ -169,7 +170,7 @@ describe('MarketplacePaymentStatusCard', () => {
expect(screen.queryByText(/Real money/)).not.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', () => {
runtime.deployEnv = undefined;
render(
<MarketplacePaymentStatusCard
Expand All @@ -182,7 +183,8 @@ describe('MarketplacePaymentStatusCard', () => {
/>,
);

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();
});

it('shows the sandbox badge without any payment notice, even on a staging deploy', () => {
Expand Down Expand Up @@ -281,6 +283,8 @@ describe('MarketplacePaymentStatusCard', () => {
expect(
screen.getByText('Bitcoin is temporarily unavailable. Other payment methods are unaffected.'),
).toBeInTheDocument();
expect(screen.getByText('The item is held for you once a payment starts.')).toBeInTheDocument();
expect(screen.queryByText(/never holds funds/i)).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: /Card \(Stripe\)/ })).toBeInTheDocument();
});

Expand Down Expand Up @@ -347,6 +351,10 @@ describe('MarketplacePaymentStatusCard', () => {
);

expect(screen.getByText(holderUnboundCopy(holdExpiresAt))).toBeInTheDocument();
expect(screen.queryByText('Real money. Payments are final and go directly to the seller.')).not.toBeInTheDocument();
expect(screen.queryByText(/Choose how to pay/i)).not.toBeInTheDocument();
expect(screen.queryByText(/pays the seller directly/i)).not.toBeInTheDocument();
expect(screen.queryByText(/never holds funds/i)).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Back' }));
await waitFor(() =>
expect(CommerceController.executeMarketplaceCommand).toHaveBeenCalledWith(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,13 @@ export function MarketplacePaymentStatusCard({
{order.fiatVerification === 'seller-attested' && <Badge variant="outline">Seller-attested</Badge>}
{isSandbox && <Badge variant="secondary">Sandbox · simulated payment · no real funds</Badge>}
</div>
{!isSandbox && isBuyer && isAwaiting && (
{!isSandbox && isBuyer && isAwaiting && isStaging && (
<Typography
as="p"
role="note"
className="rounded-xl border border-amber-500/40 bg-amber-500/10 px-4 py-3 text-sm text-amber-200"
>
{isStaging
? 'Staging environment — test rails, no real funds move'
: 'Real money. Payments are final and go directly to the seller.'}
Staging environment — test rails, no real funds move
</Typography>
)}
{visibleStatus === 'confirmed' && order.fiatVerification === 'gateway-notified' && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ vi.mock('@/hooks/useStepUpReauth/useStepUpReauth', () => ({
}));

describe('MarketplaceReauthDialog', () => {
it('renders the exact requested capability string beside the QR so the user can compare it with the signer', async () => {
it('asks for a sign-in in product language and does not print capability paths', async () => {
render(<MarketplaceReauthDialog triggerLabel="Sign in again" />);

await userEvent.setup().click(screen.getByRole('button', { name: 'Sign in again' }));

// Verbatim from the single CAPABILITIES constant — never a paraphrase.
expect(screen.getByText(CAPABILITIES)).toBeInTheDocument();
expect(screen.getByText(/Pubky Ring will show this exact permission list/)).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Sign in again' })).toBeInTheDocument();
expect(screen.getByText('Sign in again for this device.')).toBeInTheDocument();
expect(screen.queryByText(CAPABILITIES)).not.toBeInTheDocument();
expect(screen.queryByText(/compare it before approving/i)).not.toBeInTheDocument();
expect(screen.queryByText(/permission list/i)).not.toBeInTheDocument();
});
});
24 changes: 2 additions & 22 deletions src/components/organisms/Marketplace/MarketplaceReauthDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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 { useStepUpReauth } from '@/hooks/useStepUpReauth/useStepUpReauth';
import { Logger } from '@/libs/logger/logger';
import { QrCodeSlot } from '@/molecules/QrCodeSlot/QrCodeSlot';
Expand Down Expand Up @@ -74,31 +73,12 @@ export function MarketplaceReauthDialog({
</DialogTrigger>
<DialogContent className="border-border bg-popover">
<DialogHeader>
<DialogTitle>Sign in again with full permissions</DialogTitle>
<DialogTitle>Sign in again</DialogTitle>
</DialogHeader>

<Typography as="p" className="text-sm text-muted-foreground">
Approving with your signer (Pubky Ring) signs you in again with the app&apos;s full permission list — public
app data, encrypted messaging, and private storage. Your account and data stay exactly as they are; only the
session&apos;s permissions widen, so watchlist sync, portable receipts, and messaging all work.
Sign in again for this device.
</Typography>
<Typography as="p" className="text-sm text-muted-foreground">
Ring will show the full permission list — that is correct. This approval replaces your current session for the
same identity; it does not create a new account.
</Typography>

{/* The exact requested capability string, verbatim from the single
`CAPABILITIES` constant the flow is generated with — Ring displays
the same list at approval time, so the user can compare the two
(docs/ecommerce/step-up-approval.md, QR/phish-swap row). */}
<div className="rounded-md border border-border bg-muted/40 p-3">
<code className="block font-mono text-xs break-all" data-cy="reauth-requested-capabilities">
{CAPABILITIES}
</code>
<Typography as="p" className="mt-1 text-xs text-muted-foreground">
Pubky Ring will show this exact permission list — compare it before approving.
</Typography>
</div>

{reauth.status === 'error' ? (
<div className="grid gap-3">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -32,6 +32,7 @@ export function MarketplaceSessionConnectDialog({
autoOpen?: boolean;
}) {
const [open, setOpen] = useState(false);
const grantFlowEnabled = getMarketplaceGrantFlowEnabled();
const session = useMarketplaceSessionConnect({
onConnected: () => {
toast({
Expand Down Expand Up @@ -90,27 +91,10 @@ export function MarketplaceSessionConnectDialog({
<Typography as="p" className="text-sm text-muted-foreground">
{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.'}
</Typography>
<Typography as="p" className="text-sm text-muted-foreground">
{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.'}
</Typography>
{requestsFullGrant && !requestsGrantReconnect && (
<div className="rounded-md border border-border bg-muted/40 p-3">
<code className="block font-sans text-xs break-all" data-cy="session-connect-requested-capabilities">
{CAPABILITIES}
</code>
<Typography as="p" className="mt-1 text-xs text-muted-foreground">
Pubky Ring will show this exact permission list — compare it before approving.
</Typography>
</div>
)}

{['error', 'mismatch', 'expired', 'cancelled'].includes(session.status) ? (
<div className="grid gap-3">
Expand Down
5 changes: 3 additions & 2 deletions src/components/organisms/SignIn/SignIn.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<SignInContent />);
});
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 () => {
Expand Down
16 changes: 0 additions & 16 deletions src/components/organisms/SignIn/SignIn.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,6 @@ exports[`SignInContent - Snapshots > matches snapshot for the QR sign-in layout
width="48"
/>
</button>
<div
class="max-w-sm rounded-md border border-border bg-muted/40 p-3"
>
<code
class="block font-mono text-xs break-all"
data-cy="signin-requested-capabilities"
>
/pub/pubky.app/:rw,/pub/paykit/:rw,/priv/pubky.app/:rw
</code>
<p
class="mt-1 text-xs text-muted-foreground"
data-testid="typography"
>
Pubky Ring will show this exact permission list — compare it before approving.
</p>
</div>
</div>
<div
aria-hidden="true"
Expand Down
9 changes: 0 additions & 9 deletions src/components/organisms/SignIn/SignIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { Link } from '@/atoms/Link/Link';
import { PageHeader } from '@/atoms/PageHeader/PageHeader';
import { PageSubtitle } from '@/atoms/PageSubtitle/PageSubtitle';
import { Typography } from '@/atoms/Typography/Typography';
import { CAPABILITIES } from '@/config/app';
import { useMobileAuth } from '@/hooks/useMobileAuth/useMobileAuth';
import { Logger } from '@/libs/logger/logger';
import { cn } from '@/libs/utils/utils';
Expand Down Expand Up @@ -178,14 +177,6 @@ export const SignInContent = () => {
activeQrHasHoverEffect
/>
</button>
<div className="max-w-sm rounded-md border border-border bg-muted/40 p-3">
<code className="block font-mono text-xs break-all" data-cy="signin-requested-capabilities">
{CAPABILITIES}
</code>
<Typography as="p" className="mt-1 text-xs text-muted-foreground">
Pubky Ring will show this exact permission list — compare it before approving.
</Typography>
</div>
</BalancedQrCard>
</Container>

Expand Down
Loading
Loading