fix(payments): round out profile CLINK and Lightning methods
This commit is contained in:
parent
22eb46c62b
commit
b39c2ec386
9 changed files with 62 additions and 22 deletions
|
|
@ -28,6 +28,7 @@ import { LabDraftBanner } from '@/components/LabDraftBanner';
|
||||||
import { LabProfileProgress } from '@/components/LabProfileProgress';
|
import { LabProfileProgress } from '@/components/LabProfileProgress';
|
||||||
import { FormWizard, WizardStep } from '@/components/FormWizard';
|
import { FormWizard, WizardStep } from '@/components/FormWizard';
|
||||||
import { isValidNoffer, type EntityType } from '@/lib/types';
|
import { isValidNoffer, type EntityType } from '@/lib/types';
|
||||||
|
import { normalizeClinkCode } from '@/lib/clink';
|
||||||
import type { LabOption } from '@/lib/labExtras';
|
import type { LabOption } from '@/lib/labExtras';
|
||||||
import { labProfilePathFromKind } from '@/lib/labRoutes';
|
import { labProfilePathFromKind } from '@/lib/labRoutes';
|
||||||
import { normalizeUrlInput, sanitizeUrl, isValidEmail } from '@/lib/sanitizeUrl';
|
import { normalizeUrlInput, sanitizeUrl, isValidEmail } from '@/lib/sanitizeUrl';
|
||||||
|
|
@ -327,7 +328,7 @@ export function CraftProfileForm({ config }: { config: CraftFormConfig }) {
|
||||||
if (website.trim()) tags.push(['website', normalizeUrlInput(website)]);
|
if (website.trim()) tags.push(['website', normalizeUrlInput(website)]);
|
||||||
if (portfolio.trim()) tags.push(['portfolio', normalizeUrlInput(portfolio)]);
|
if (portfolio.trim()) tags.push(['portfolio', normalizeUrlInput(portfolio)]);
|
||||||
if (social.trim()) tags.push(['social', normalizeUrlInput(social)]);
|
if (social.trim()) tags.push(['social', normalizeUrlInput(social)]);
|
||||||
if (isValidNoffer(noffer.trim())) tags.push(['noffer', noffer.trim().toLowerCase()]);
|
if (isValidNoffer(noffer.trim())) tags.push(['noffer', normalizeClinkCode(noffer)]);
|
||||||
|
|
||||||
// Craft-specific extras
|
// Craft-specific extras
|
||||||
for (const f of extraFields) {
|
for (const f of extraFields) {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { isCrewVideoUrl, parseCrewVideo } from '@/lib/videoEmbed';
|
||||||
import { CrewSponsorPicker } from '@/components/crew/CrewSponsorPicker';
|
import { CrewSponsorPicker } from '@/components/crew/CrewSponsorPicker';
|
||||||
import { NofferField } from '@/components/NofferField';
|
import { NofferField } from '@/components/NofferField';
|
||||||
import { SKATE_CREW_KIND, SKATE_SPOT_KIND, SKATE_SHOP_KIND, parseAddrCoord, ZAP_GOAL_KIND, CREW_VIBE_LABELS, CREW_FOCUS_LABELS, CREW_FOCUS_DESCRIPTIONS, CREW_FOCUS_TAG, CREW_LOOKING_FOR_OPTIONS, CREW_ROLE_META, CREW_ROLE_ORDER, SHOW_CREW_TREASURY, isValidNoffer, type SkateCrew, type CrewFocus, type CrewRole } from '@/lib/types';
|
import { SKATE_CREW_KIND, SKATE_SPOT_KIND, SKATE_SHOP_KIND, parseAddrCoord, ZAP_GOAL_KIND, CREW_VIBE_LABELS, CREW_FOCUS_LABELS, CREW_FOCUS_DESCRIPTIONS, CREW_FOCUS_TAG, CREW_LOOKING_FOR_OPTIONS, CREW_ROLE_META, CREW_ROLE_ORDER, SHOW_CREW_TREASURY, isValidNoffer, type SkateCrew, type CrewFocus, type CrewRole } from '@/lib/types';
|
||||||
|
import { normalizeClinkCode } from '@/lib/clink';
|
||||||
import { CREW_VIBE_KEYS, CREW_FOCUS_KEYS, CREW_FOCUS_DESC_KEYS, CREW_LOOKING_FOR_KEYS, CREW_ROLE_KEYS } from '@/lib/labelKeys';
|
import { CREW_VIBE_KEYS, CREW_FOCUS_KEYS, CREW_FOCUS_DESC_KEYS, CREW_LOOKING_FOR_KEYS, CREW_ROLE_KEYS } from '@/lib/labelKeys';
|
||||||
import { useCrewGoal } from '@/hooks/useCrewGoal';
|
import { useCrewGoal } from '@/hooks/useCrewGoal';
|
||||||
import { useCoordinator, useCoordinatorHealth } from '@/hooks/useCoordinator';
|
import { useCoordinator, useCoordinatorHealth } from '@/hooks/useCoordinator';
|
||||||
|
|
@ -571,7 +572,7 @@ export function CrewForm({ existing }: CrewFormProps) {
|
||||||
if (s.trim()) tags.push(['social', s.trim()]);
|
if (s.trim()) tags.push(['social', s.trim()]);
|
||||||
}
|
}
|
||||||
for (const handle of nip05s) tags.push(['nip05', handle]);
|
for (const handle of nip05s) tags.push(['nip05', handle]);
|
||||||
if (isValidNoffer(noffer.trim())) tags.push(['noffer', noffer.trim().toLowerCase()]);
|
if (isValidNoffer(noffer.trim())) tags.push(['noffer', normalizeClinkCode(noffer)]);
|
||||||
|
|
||||||
// Members: admins get an `admin` role marker on their `p` tag.
|
// Members: admins get an `admin` role marker on their `p` tag.
|
||||||
for (const pk of members) {
|
for (const pk of members) {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import { Nip05HandlesEditor } from '@/components/Nip05HandlesEditor';
|
||||||
import { SkateNip05Input } from '@/components/SkateNip05Input';
|
import { SkateNip05Input } from '@/components/SkateNip05Input';
|
||||||
import { SKATE_NIP05_DOMAIN, parseSkateNip05 } from '@/lib/skateNip05';
|
import { SKATE_NIP05_DOMAIN, parseSkateNip05 } from '@/lib/skateNip05';
|
||||||
import { isValidNoffer, isValidSilentPayment, isValidLightningAddress } from '@/lib/types';
|
import { isValidNoffer, isValidSilentPayment, isValidLightningAddress } from '@/lib/types';
|
||||||
|
import { normalizeClinkCode } from '@/lib/clink';
|
||||||
import { useTranslation } from '@/contexts/language';
|
import { useTranslation } from '@/contexts/language';
|
||||||
|
|
||||||
export const EditProfileForm: React.FC = () => {
|
export const EditProfileForm: React.FC = () => {
|
||||||
|
|
@ -52,7 +53,7 @@ export const EditProfileForm: React.FC = () => {
|
||||||
// Lightning address (LUD-16). Part of NostrMetadata but not in the default
|
// Lightning address (LUD-16). Part of NostrMetadata but not in the default
|
||||||
// form fields, so we manage it here and merge it on save.
|
// form fields, so we manage it here and merge it on save.
|
||||||
const [lud16, setLud16] = useState('');
|
const [lud16, setLud16] = useState('');
|
||||||
const lud16Trimmed = lud16.trim();
|
const lud16Trimmed = lud16.trim().replace(/^lightning:\/\//i, '').replace(/^lightning:/i, '');
|
||||||
const lud16Invalid = lud16Trimmed.length > 0 && !isValidLightningAddress(lud16Trimmed);
|
const lud16Invalid = lud16Trimmed.length > 0 && !isValidLightningAddress(lud16Trimmed);
|
||||||
|
|
||||||
// Bitcoin Silent Payment address (BIP-352). Custom kind-0 field.
|
// Bitcoin Silent Payment address (BIP-352). Custom kind-0 field.
|
||||||
|
|
@ -224,7 +225,7 @@ export const EditProfileForm: React.FC = () => {
|
||||||
|
|
||||||
// Merge the custom CLINK offer code (or remove it when cleared).
|
// Merge the custom CLINK offer code (or remove it when cleared).
|
||||||
if (nofferValid) {
|
if (nofferValid) {
|
||||||
data.noffer = nofferTrimmed.toLowerCase();
|
data.noffer = normalizeClinkCode(nofferTrimmed);
|
||||||
} else {
|
} else {
|
||||||
delete data.noffer;
|
delete data.noffer;
|
||||||
}
|
}
|
||||||
|
|
@ -493,6 +494,7 @@ export const EditProfileForm: React.FC = () => {
|
||||||
value={lud16}
|
value={lud16}
|
||||||
onChange={setLud16}
|
onChange={setLud16}
|
||||||
placeholder="you@yourdomain.com"
|
placeholder="you@yourdomain.com"
|
||||||
|
inputMode="email"
|
||||||
status={
|
status={
|
||||||
lud16Invalid
|
lud16Invalid
|
||||||
? { kind: 'error', text: t('settingsUi.editProfile.lud16Invalid') }
|
? { kind: 'error', text: t('settingsUi.editProfile.lud16Invalid') }
|
||||||
|
|
@ -713,7 +715,8 @@ const FieldRow: React.FC<{
|
||||||
onChange: (v: string) => void;
|
onChange: (v: string) => void;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
status: { kind: 'hint' | 'ok' | 'error'; text: string };
|
status: { kind: 'hint' | 'ok' | 'error'; text: string };
|
||||||
}> = ({ id, icon, iconClass, label, value, onChange, placeholder, status }) => (
|
inputMode?: React.HTMLAttributes<HTMLInputElement>['inputMode'];
|
||||||
|
}> = ({ id, icon, iconClass, label, value, onChange, placeholder, status, inputMode }) => (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={id} className="flex items-center gap-2 text-sm font-semibold">
|
<Label htmlFor={id} className="flex items-center gap-2 text-sm font-semibold">
|
||||||
<span className={`h-6 w-6 rounded-lg flex items-center justify-center ${iconClass}`}>{icon}</span>
|
<span className={`h-6 w-6 rounded-lg flex items-center justify-center ${iconClass}`}>{icon}</span>
|
||||||
|
|
@ -726,6 +729,7 @@ const FieldRow: React.FC<{
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
|
inputMode={inputMode}
|
||||||
className="font-mono text-sm"
|
className="font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
{status.kind === 'error' ? (
|
{status.kind === 'error' ? (
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export function isValidLightningAddress(address: string): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const trimmed = address.trim();
|
const trimmed = address.trim().replace(/^lightning:\/\//i, '').replace(/^lightning:/i, '');
|
||||||
const parts = trimmed.split('@');
|
const parts = trimmed.split('@');
|
||||||
if (parts.length !== 2) {
|
if (parts.length !== 2) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -60,7 +60,8 @@ export async function resolveLightningAddress(
|
||||||
throw new Error('Invalid Lightning address format. Expected: username@domain.com');
|
throw new Error('Invalid Lightning address format. Expected: username@domain.com');
|
||||||
}
|
}
|
||||||
|
|
||||||
const [username, domain] = lightningAddress.split('@');
|
const normalized = lightningAddress.trim().replace(/^lightning:\/\//i, '').replace(/^lightning:/i, '');
|
||||||
|
const [username, domain] = normalized.split('@');
|
||||||
const lnurlEndpoint = `https://${domain}/.well-known/lnurlp/${username}`;
|
const lnurlEndpoint = `https://${domain}/.well-known/lnurlp/${username}`;
|
||||||
|
|
||||||
const response = await fetch(lnurlEndpoint, {
|
const response = await fetch(lnurlEndpoint, {
|
||||||
|
|
|
||||||
|
|
@ -587,7 +587,8 @@ export function isValidSilentPayment(value: string | undefined | null): value is
|
||||||
*/
|
*/
|
||||||
export function isValidLightningAddress(value: string | undefined | null): value is string {
|
export function isValidLightningAddress(value: string | undefined | null): value is string {
|
||||||
if (!value) return false;
|
if (!value) return false;
|
||||||
return /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(value.trim());
|
const normalized = value.trim().replace(/^lightning:\/\//i, '').replace(/^lightning:/i, '');
|
||||||
|
return /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Skater profile (kind 35000) ───────────────────────────────────────────
|
// ─── Skater profile (kind 35000) ───────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { useToast } from '@/hooks/useToast';
|
||||||
import { ShopBountyCard } from '@/components/ShopBountyCard';
|
import { ShopBountyCard } from '@/components/ShopBountyCard';
|
||||||
import { LoginArea } from '@/components/auth/LoginArea';
|
import { LoginArea } from '@/components/auth/LoginArea';
|
||||||
import { SKATE_SHOP_KIND, isValidNoffer } from '@/lib/types';
|
import { SKATE_SHOP_KIND, isValidNoffer } from '@/lib/types';
|
||||||
|
import { normalizeClinkCode } from '@/lib/clink';
|
||||||
import { reverseGeocodeRegions, pushRegionTags } from '@/lib/geocodeRegions';
|
import { reverseGeocodeRegions, pushRegionTags } from '@/lib/geocodeRegions';
|
||||||
import { NofferField } from '@/components/NofferField';
|
import { NofferField } from '@/components/NofferField';
|
||||||
import {
|
import {
|
||||||
|
|
@ -221,7 +222,7 @@ export default function CreateShopPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isValidNoffer(noffer.trim())) {
|
if (isValidNoffer(noffer.trim())) {
|
||||||
tags.push(['noffer', noffer.trim().toLowerCase()]);
|
tags.push(['noffer', normalizeClinkCode(noffer)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const handle of nip05s) tags.push(['nip05', handle]);
|
for (const handle of nip05s) tags.push(['nip05', handle]);
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { useToast } from '@/hooks/useToast';
|
||||||
import { useShop } from '@/hooks/useShops';
|
import { useShop } from '@/hooks/useShops';
|
||||||
import { LoginArea } from '@/components/auth/LoginArea';
|
import { LoginArea } from '@/components/auth/LoginArea';
|
||||||
import { SKATE_SHOP_KIND, isValidNoffer } from '@/lib/types';
|
import { SKATE_SHOP_KIND, isValidNoffer } from '@/lib/types';
|
||||||
|
import { normalizeClinkCode } from '@/lib/clink';
|
||||||
import { reverseGeocodeRegions, pushRegionTags } from '@/lib/geocodeRegions';
|
import { reverseGeocodeRegions, pushRegionTags } from '@/lib/geocodeRegions';
|
||||||
import { NofferField } from '@/components/NofferField';
|
import { NofferField } from '@/components/NofferField';
|
||||||
import {
|
import {
|
||||||
|
|
@ -266,7 +267,7 @@ export default function EditShopPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isValidNoffer(noffer.trim())) {
|
if (isValidNoffer(noffer.trim())) {
|
||||||
tags.push(['noffer', noffer.trim().toLowerCase()]);
|
tags.push(['noffer', normalizeClinkCode(noffer)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const handle of nip05s) tags.push(['nip05', handle]);
|
for (const handle of nip05s) tags.push(['nip05', handle]);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { TrustFlaggedNotice } from '@/components/TrustFlaggedNotice';
|
||||||
import { TradeReputationCard } from '@/components/TradeReputationCard';
|
import { TradeReputationCard } from '@/components/TradeReputationCard';
|
||||||
import { ListingTradeActions } from '@/components/ListingTradeActions';
|
import { ListingTradeActions } from '@/components/ListingTradeActions';
|
||||||
import { ListingOffersPanel } from '@/components/ListingOffersPanel';
|
import { ListingOffersPanel } from '@/components/ListingOffersPanel';
|
||||||
|
import { ClinkPayButton } from '@/components/ClinkPayButton';
|
||||||
import { OfferDialog } from '@/components/OfferDialog';
|
import { OfferDialog } from '@/components/OfferDialog';
|
||||||
import { MarkSoldDialog } from '@/components/MarkSoldDialog';
|
import { MarkSoldDialog } from '@/components/MarkSoldDialog';
|
||||||
import { DeleteEntityDialog } from '@/components/DeleteEntityDialog';
|
import { DeleteEntityDialog } from '@/components/DeleteEntityDialog';
|
||||||
|
|
@ -24,7 +25,7 @@ import { useAuthor } from '@/hooks/useAuthor';
|
||||||
import { useSellerInfo } from '@/hooks/useSellerInfo';
|
import { useSellerInfo } from '@/hooks/useSellerInfo';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { ErrorState } from '@/components/ErrorState';
|
import { ErrorState } from '@/components/ErrorState';
|
||||||
import { formatPrice, GEAR_CONDITIONS, GEAR_CATEGORIES } from '@/lib/types';
|
import { formatPrice, GEAR_CONDITIONS, GEAR_CATEGORIES, isValidNoffer } from '@/lib/types';
|
||||||
import { GEAR_CATEGORY_KEYS, GEAR_CONDITION_KEYS } from '@/lib/labelKeys';
|
import { GEAR_CATEGORY_KEYS, GEAR_CONDITION_KEYS } from '@/lib/labelKeys';
|
||||||
import { genUserName } from '@/lib/genUserName';
|
import { genUserName } from '@/lib/genUserName';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
@ -123,6 +124,9 @@ export default function ListingDetailPage() {
|
||||||
: null;
|
: null;
|
||||||
const displayName = metadata?.name ?? (pubkey ? genUserName(pubkey) : t('marketUi.listingDetail.unknown'));
|
const displayName = metadata?.name ?? (pubkey ? genUserName(pubkey) : t('marketUi.listingDetail.unknown'));
|
||||||
const npub = pubkey ? nip19.npubEncode(pubkey) : '';
|
const npub = pubkey ? nip19.npubEncode(pubkey) : '';
|
||||||
|
const sellerNoffer = typeof (metadata as Record<string, unknown> | undefined)?.noffer === 'string'
|
||||||
|
? ((metadata as Record<string, unknown>).noffer as string)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const lightboxImages: LightboxImage[] = item.images.map((url) => ({ url, isOwner: isOwnListing }));
|
const lightboxImages: LightboxImage[] = item.images.map((url) => ({ url, isOwner: isOwnListing }));
|
||||||
|
|
||||||
|
|
@ -337,6 +341,13 @@ export default function ListingDetailPage() {
|
||||||
<Card className="bg-black/90 border-white/10">
|
<Card className="bg-black/90 border-white/10">
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<ListingTradeActions item={item} />
|
<ListingTradeActions item={item} />
|
||||||
|
{isValidNoffer(sellerNoffer) && (
|
||||||
|
<ClinkPayButton
|
||||||
|
noffer={sellerNoffer}
|
||||||
|
recipientName={displayName}
|
||||||
|
className="mt-2 w-full justify-center"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{user && item.priceType !== 'fixed' && (
|
{user && item.priceType !== 'fixed' && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ import { EventCard } from '@/components/EventCard';
|
||||||
import { genUserName } from '@/lib/genUserName';
|
import { genUserName } from '@/lib/genUserName';
|
||||||
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
import { sanitizeUrl } from '@/lib/sanitizeUrl';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { SKATER_STANCES, SKATER_STYLES, SKATER_LEVELS, NEWSLETTER_KIND } from '@/lib/types';
|
import { SKATER_STANCES, SKATER_STYLES, SKATER_LEVELS, NEWSLETTER_KIND, isValidNoffer, isValidLightningAddress } from '@/lib/types';
|
||||||
import { NewsletterIssueView } from '@/components/NewsletterIssueView';
|
import { NewsletterIssueView } from '@/components/NewsletterIssueView';
|
||||||
import NotFound from './NotFound';
|
import NotFound from './NotFound';
|
||||||
import { MapPin, Store, MessageCircle, Globe, Zap, ExternalLink, Copy, Check, ShieldCheck, ShieldAlert, Loader2, Instagram, Share2, Users, Crown, Bot, Pencil, Package, Tag, ArrowRightLeft, Bitcoin, Award, MessageSquareLock, PersonStanding, Sparkles, Home, Clock, Server, Plus, UserPlus, UserCheck, Handshake } from 'lucide-react';
|
import { MapPin, Store, MessageCircle, Globe, Zap, ExternalLink, Copy, Check, ShieldCheck, ShieldAlert, Loader2, Instagram, Share2, Users, Crown, Bot, Pencil, Package, Tag, ArrowRightLeft, Bitcoin, Award, MessageSquareLock, PersonStanding, Sparkles, Home, Clock, Server, Plus, UserPlus, UserCheck, Handshake } from 'lucide-react';
|
||||||
|
|
@ -150,6 +150,8 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
||||||
const raw = metadata as Record<string, unknown> | undefined;
|
const raw = metadata as Record<string, unknown> | undefined;
|
||||||
const noffer = raw?.noffer;
|
const noffer = raw?.noffer;
|
||||||
const nofferCode = typeof noffer === 'string' ? noffer : undefined;
|
const nofferCode = typeof noffer === 'string' ? noffer : undefined;
|
||||||
|
const hasClink = isValidNoffer(nofferCode);
|
||||||
|
const hasLightning = isValidLightningAddress(lud16) || !!metadata?.lud06;
|
||||||
// Extra custom profile fields.
|
// Extra custom profile fields.
|
||||||
const silentPayment = typeof raw?.bitcoin_silent_payment === 'string' ? raw.bitcoin_silent_payment : undefined;
|
const silentPayment = typeof raw?.bitcoin_silent_payment === 'string' ? raw.bitcoin_silent_payment : undefined;
|
||||||
const userBadges = parseBadges(metadata);
|
const userBadges = parseBadges(metadata);
|
||||||
|
|
@ -160,6 +162,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
||||||
// one-tap "Add" instead of just a link.
|
// one-tap "Add" instead of just a link.
|
||||||
const cordnPointer = cordnUrl ? parseCoordinatorInput(cordnUrl) : null;
|
const cordnPointer = cordnUrl ? parseCoordinatorInput(cordnUrl) : null;
|
||||||
const [spCopied, setSpCopied] = useState(false);
|
const [spCopied, setSpCopied] = useState(false);
|
||||||
|
const [lud16Copied, setLud16Copied] = useState(false);
|
||||||
const [shareOpen, setShareOpen] = useState(false);
|
const [shareOpen, setShareOpen] = useState(false);
|
||||||
|
|
||||||
const npub = nip19.npubEncode(pubkey);
|
const npub = nip19.npubEncode(pubkey);
|
||||||
|
|
@ -892,14 +895,30 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{lud16 && (
|
{hasLightning && lud16 && (
|
||||||
<div className="flex items-center gap-3 p-3 rounded-lg bg-gray-900">
|
<button
|
||||||
<Zap className="h-4 w-4 text-yellow-400 flex-shrink-0" />
|
type="button"
|
||||||
<div className="min-w-0">
|
onClick={() => {
|
||||||
<p className="text-[10px] uppercase tracking-wide text-gray-500 font-bold">{t('walletUi.profile.lightning')}</p>
|
void navigator.clipboard.writeText(lud16).then(() => {
|
||||||
<span className="text-gray-300 text-sm truncate block">{lud16}</span>
|
setLud16Copied(true);
|
||||||
|
setTimeout(() => setLud16Copied(false), 2000);
|
||||||
|
}).catch(() => undefined);
|
||||||
|
}}
|
||||||
|
className="flex w-full items-center gap-3 rounded-lg bg-gray-900 p-3 text-left transition-colors hover:bg-gray-800"
|
||||||
|
>
|
||||||
|
<Zap className="h-4 w-4 flex-shrink-0 text-yellow-400" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-wide text-gray-500">
|
||||||
|
{t('walletUi.profile.lightning')} {lud16Copied && <span className="text-green-400">· {t('common.copied')}</span>}
|
||||||
|
</p>
|
||||||
|
<span className="block truncate text-sm text-gray-300">{lud16}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{lud16Copied ? (
|
||||||
|
<Check className="h-3.5 w-3.5 flex-shrink-0 text-green-400" />
|
||||||
|
) : (
|
||||||
|
<Copy className="h-3.5 w-3.5 flex-shrink-0 text-gray-500" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bitcoin Silent Payment address — copyable */}
|
{/* Bitcoin Silent Payment address — copyable */}
|
||||||
|
|
@ -1052,7 +1071,7 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tip Card */}
|
{/* Tip Card */}
|
||||||
{!isOwnProfile && (profileEvent || nofferCode) && (
|
{!isOwnProfile && (hasLightning || hasClink) && (
|
||||||
<Card className="bg-gradient-to-br from-[#ccff00]/10 to-transparent border-2 border-[#ccff00]/30">
|
<Card className="bg-gradient-to-br from-[#ccff00]/10 to-transparent border-2 border-[#ccff00]/30">
|
||||||
<CardContent className="p-4 text-center">
|
<CardContent className="p-4 text-center">
|
||||||
<Zap className="h-8 w-8 mx-auto mb-2 text-[#ccff00]" />
|
<Zap className="h-8 w-8 mx-auto mb-2 text-[#ccff00]" />
|
||||||
|
|
@ -1061,13 +1080,13 @@ function ProfilePage({ pubkey }: { pubkey: string }) {
|
||||||
{t('walletUi.profile.supportSkaterDesc')}
|
{t('walletUi.profile.supportSkaterDesc')}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{profileEvent && (
|
{profileEvent && hasLightning && (
|
||||||
<ZapButton
|
<ZapButton
|
||||||
target={profileEvent}
|
target={profileEvent}
|
||||||
className="w-full justify-center bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold"
|
className="w-full justify-center bg-[#ccff00] text-black hover:bg-[#ccff00]/90 font-bold"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{nofferCode && (
|
{hasClink && nofferCode && (
|
||||||
<ClinkPayButton
|
<ClinkPayButton
|
||||||
noffer={nofferCode}
|
noffer={nofferCode}
|
||||||
recipientName={displayName}
|
recipientName={displayName}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue