SkateSpots/src/components/NofferField.tsx
shakespeare.diy feddbaf5ef Add optional CLINK (noffer) self-custodial payments to Lab profiles + shops
Phase 1 CLINK integration (recognize & display only):
- types.ts: optional noffer field + isValidNoffer guard on SkateCoach,
  SkateFilmer, SkateEditor, SkatePhotographer, SkateShop; parsers read it
- NofferField: reusable opt-in input with live validation, added to all 4
  Become-* Lab forms and both Create/Edit shop forms
- ClinkPayButton: secondary "Pay via CLINK" action (QR + copy + nostr: deep
  link), rendered on CoachDetailPage and ShopDetailPage only when a valid
  noffer is present; never replaces the existing zap / Lightning Address flow
- useQRCodeScanner: classify scanned noffer1/ndebit1 CLINK codes
- NIP.md: document the noffer tag on Lab kinds + shops and a CLINK section

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-07-24 12:34:50 +07:00

62 lines
2.3 KiB
TypeScript

import { Zap, ShieldCheck, AlertCircle } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { isValidNoffer } from '@/lib/types';
interface NofferFieldProps {
value: string;
onChange: (value: string) => void;
}
/**
* Optional CLINK Offer (noffer1...) input for provider / shop forms.
* Self-custodial, Nostr-native static payment code — see https://clinkme.dev.
* Purely additive: leaving it blank changes nothing.
*/
export function NofferField({ value, onChange }: NofferFieldProps) {
const trimmed = value.trim();
const showError = trimmed.length > 0 && !isValidNoffer(trimmed);
const showValid = isValidNoffer(trimmed);
return (
<div className="space-y-2 rounded-lg border border-indigo-500/30 bg-indigo-500/5 p-4">
<Label htmlFor="noffer" className="flex items-center gap-2 text-indigo-400">
<Zap className="h-4 w-4" />
CLINK Offer Code <span className="text-xs font-normal text-gray-500">(optional, advanced)</span>
</Label>
<Input
id="noffer"
placeholder="noffer1..."
value={value}
onChange={(e) => onChange(e.target.value)}
spellCheck={false}
autoCapitalize="none"
className="bg-gray-800 border-gray-700 font-mono text-sm"
/>
{showError ? (
<p className="flex items-center gap-1.5 text-xs text-red-400">
<AlertCircle className="h-3.5 w-3.5 shrink-0" />
That doesn't look like a valid <code className="mx-1">noffer1…</code> code.
</p>
) : showValid ? (
<p className="flex items-center gap-1.5 text-xs text-green-400">
<ShieldCheck className="h-3.5 w-3.5 shrink-0" />
Valid CLINK code — a "Pay via CLINK" button will show on your profile.
</p>
) : (
<p className="text-xs text-gray-500">
Paste a self-custodial CLINK payment code (e.g. from ShockWallet / Lightning.Pub) to accept
Nostr-native Lightning payments — no web server or custodian.{' '}
<a
href="https://clinkme.dev"
target="_blank"
rel="noopener noreferrer"
className="text-indigo-400 underline hover:text-indigo-300"
>
Learn more
</a>
</p>
)}
</div>
);
}