Merge branch 'feat-choose-zaps-amount' into 'main'
Enhance zap experience - Render QR code, choose amount, open wallet, etc Closes #1666 See merge request soapbox-pub/soapbox!3046
This commit is contained in:
commit
9945a62ecd
|
@ -314,18 +314,28 @@ const undislikeFail = (status: StatusEntity, error: unknown) => ({
|
||||||
skipLoading: true,
|
skipLoading: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const zap = (status: StatusEntity, amount: number) =>
|
const zap = (status: StatusEntity, amount: number, comment: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
if (!isLoggedIn(getState)) return;
|
if (!isLoggedIn(getState)) return;
|
||||||
|
|
||||||
dispatch(zapRequest(status));
|
dispatch(zapRequest(status));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/statuses/${status.id}/zap`, { amount }).then(function(response) {
|
return api(getState).post(`/api/v1/statuses/${status.id}/zap`, { amount, comment: comment ?? '' }).then(async function(response) {
|
||||||
|
const invoice = response.headers['ln-invoice'];
|
||||||
|
if (!invoice) throw Error('Could not generate invoice');
|
||||||
|
if (!window.webln) return invoice;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.webln?.enable();
|
||||||
|
await window.webln?.sendPayment(invoice);
|
||||||
dispatch(zapSuccess(status));
|
dispatch(zapSuccess(status));
|
||||||
}).catch(function(error) {
|
return undefined;
|
||||||
dispatch(zapFail(status, error));
|
} catch (e) { // In case it fails we just return the invoice so the QR code can be created
|
||||||
|
return invoice;
|
||||||
|
}
|
||||||
|
}).catch(function(e) {
|
||||||
|
dispatch(zapFail(status, e));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const zapRequest = (status: StatusEntity) => ({
|
const zapRequest = (status: StatusEntity) => ({
|
||||||
type: ZAP_REQUEST,
|
type: ZAP_REQUEST,
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { blockAccount } from 'soapbox/actions/accounts';
|
||||||
import { launchChat } from 'soapbox/actions/chats';
|
import { launchChat } from 'soapbox/actions/chats';
|
||||||
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'soapbox/actions/compose';
|
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'soapbox/actions/compose';
|
||||||
import { editEvent } from 'soapbox/actions/events';
|
import { editEvent } from 'soapbox/actions/events';
|
||||||
import { pinToGroup, toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog, unpinFromGroup, zap } from 'soapbox/actions/interactions';
|
import { pinToGroup, toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog, unpinFromGroup } from 'soapbox/actions/interactions';
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
|
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
|
||||||
import { initMuteModal } from 'soapbox/actions/mutes';
|
import { initMuteModal } from 'soapbox/actions/mutes';
|
||||||
|
@ -195,9 +195,9 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
|
|
||||||
const handleZapClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleZapClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
if (me) {
|
if (me) {
|
||||||
dispatch(zap(status, 1337));
|
dispatch(openModal('ZAP_PAY_REQUEST', { status, account: status.account }));
|
||||||
} else {
|
} else {
|
||||||
onOpenUnauthorizedModal('ZAP');
|
onOpenUnauthorizedModal('ZAP_PAY_REQUEST');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -800,7 +800,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(acceptsZaps && window.webln) && (
|
{(acceptsZaps) && (
|
||||||
<StatusActionButton
|
<StatusActionButton
|
||||||
title={intl.formatMessage(messages.zap)}
|
title={intl.formatMessage(messages.zap)}
|
||||||
icon={require('@tabler/icons/outline/bolt.svg')}
|
icon={require('@tabler/icons/outline/bolt.svg')}
|
||||||
|
|
|
@ -42,6 +42,8 @@ import {
|
||||||
UnauthorizedModal,
|
UnauthorizedModal,
|
||||||
VideoModal,
|
VideoModal,
|
||||||
EditRuleModal,
|
EditRuleModal,
|
||||||
|
ZapPayRequestModal,
|
||||||
|
ZapInvoiceModal,
|
||||||
} from 'soapbox/features/ui/util/async-components';
|
} from 'soapbox/features/ui/util/async-components';
|
||||||
|
|
||||||
import ModalLoading from './modal-loading';
|
import ModalLoading from './modal-loading';
|
||||||
|
@ -88,6 +90,8 @@ const MODAL_COMPONENTS: Record<string, React.LazyExoticComponent<any>> = {
|
||||||
'SELECT_BOOKMARK_FOLDER': SelectBookmarkFolderModal,
|
'SELECT_BOOKMARK_FOLDER': SelectBookmarkFolderModal,
|
||||||
'UNAUTHORIZED': UnauthorizedModal,
|
'UNAUTHORIZED': UnauthorizedModal,
|
||||||
'VIDEO': VideoModal,
|
'VIDEO': VideoModal,
|
||||||
|
'ZAP_INVOICE': ZapInvoiceModal,
|
||||||
|
'ZAP_PAY_REQUEST': ZapPayRequestModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ModalType = keyof typeof MODAL_COMPONENTS | null;
|
export type ModalType = keyof typeof MODAL_COMPONENTS | null;
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { QRCodeCanvas } from 'qrcode.react';
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { closeModal } from 'soapbox/actions/modals';
|
||||||
|
import CopyableInput from 'soapbox/components/copyable-input';
|
||||||
|
import { Modal, Button } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
zap_open_wallet: { id: 'zap.open_wallet', defaultMessage: 'Open Wallet' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IZapInvoice{
|
||||||
|
account: AccountEntity;
|
||||||
|
invoice: string;
|
||||||
|
onClose:(type?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZapInvoiceModal: React.FC<IZapInvoice> = ({ account, invoice, onClose }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const onClickClose = () => {
|
||||||
|
onClose('ZAP_INVOICE');
|
||||||
|
dispatch(closeModal('ZAP_PAY_REQUEST'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTitle = () => {
|
||||||
|
return <FormattedMessage id='zap.send_to' defaultMessage='Send zaps to {target}' values={{ target: account.display_name }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title={renderTitle()} onClose={onClickClose}>
|
||||||
|
<QRCodeCanvas value={invoice} />
|
||||||
|
<CopyableInput value={invoice} />
|
||||||
|
<a href={'lightning:' + invoice}>
|
||||||
|
<Button type='submit' theme='primary' icon={require('@tabler/icons/outline/folder-open.svg')} text={intl.formatMessage(messages.zap_open_wallet)} />
|
||||||
|
</a>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ZapInvoiceModal;
|
|
@ -0,0 +1,32 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Modal } from 'soapbox/components/ui';
|
||||||
|
|
||||||
|
import ZapPayRequestForm from '../../../zap/components/zap-pay-request-form';
|
||||||
|
|
||||||
|
import type { Status as StatusEntity, Account as AccountEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
interface IZapPayRequestModal {
|
||||||
|
account: AccountEntity;
|
||||||
|
status?: StatusEntity;
|
||||||
|
onClose:(type?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZapPayRequestModal: React.FC<IZapPayRequestModal> = ({ account, status, onClose }) => {
|
||||||
|
const onClickClose = () => {
|
||||||
|
onClose('ZAP_PAY_REQUEST');
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderTitle = () => {
|
||||||
|
return <FormattedMessage id='zap.send_to' defaultMessage='Send zaps to {target}' values={{ target: account.display_name }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal title={renderTitle()} onClose={onClickClose}>
|
||||||
|
<ZapPayRequestForm account={account} status={status} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ZapPayRequestModal;
|
|
@ -176,4 +176,6 @@ export const Relays = lazy(() => import('soapbox/features/admin/relays'));
|
||||||
export const Rules = lazy(() => import('soapbox/features/admin/rules'));
|
export const Rules = lazy(() => import('soapbox/features/admin/rules'));
|
||||||
export const EditRuleModal = lazy(() => import('soapbox/features/ui/components/modals/edit-rule-modal'));
|
export const EditRuleModal = lazy(() => import('soapbox/features/ui/components/modals/edit-rule-modal'));
|
||||||
export const AdminNostrRelays = lazy(() => import('soapbox/features/admin/nostr-relays'));
|
export const AdminNostrRelays = lazy(() => import('soapbox/features/admin/nostr-relays'));
|
||||||
|
export const ZapPayRequestModal = lazy(() => import('soapbox/features/ui/components/modals/zap-pay-request'));
|
||||||
|
export const ZapInvoiceModal = lazy(() => import('soapbox/features/ui/components/modals/zap-invoice'));
|
||||||
export const NostrPanel = lazy(() => import('soapbox/features/ui/components/nostr-panel'));
|
export const NostrPanel = lazy(() => import('soapbox/features/ui/components/nostr-panel'));
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { zap } from 'soapbox/actions/interactions';
|
||||||
|
import { openModal, closeModal } from 'soapbox/actions/modals';
|
||||||
|
import Account from 'soapbox/components/account';
|
||||||
|
import { Stack, Button, Select } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
interface IZapPayRequestForm {
|
||||||
|
status?: StatusEntity;
|
||||||
|
account: AccountEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
zap_button: { id: 'status.zap', defaultMessage: 'Zap' },
|
||||||
|
zap_commentPlaceholder: { id: 'zap.comment_input.placeholder', defaultMessage: 'Optional comment' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const ZapPayRequestForm = ({ account, status }: IZapPayRequestForm) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [zapComment, setZapComment] = useState('');
|
||||||
|
const [zapAmount, setZapAmount] = useState(1);
|
||||||
|
|
||||||
|
const handleSubmit = async (e?: React.FormEvent<Element>) => {
|
||||||
|
e?.preventDefault();
|
||||||
|
if (status) {
|
||||||
|
const invoice = await dispatch(zap(status, zapAmount * 1000, zapComment));
|
||||||
|
// If invoice is undefined it means the user has paid through his extension
|
||||||
|
// In this case, we simply close the modal
|
||||||
|
if (!invoice) {
|
||||||
|
dispatch(closeModal('ZAP_PAY_REQUEST'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// open QR code modal
|
||||||
|
dispatch(openModal('ZAP_INVOICE', { invoice, account }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const zapOptions = () => {
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
<option key={1} disabled>
|
||||||
|
<FormattedMessage id='zap.unit' defaultMessage='Zap amount in sats' />
|
||||||
|
</option>,
|
||||||
|
<option key={2} value={1} defaultValue={1}>1 😐</option>,
|
||||||
|
<option key={3} value={500}>500 👍</option>,
|
||||||
|
<option key={4} value={666}>666 😈 </option>,
|
||||||
|
<option key={5} value={1000}>1k 🚀</option>,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack element='form' onSubmit={handleSubmit}>
|
||||||
|
<Account account={account} showProfileHoverCard={false} />
|
||||||
|
<Select
|
||||||
|
onChange={e => setZapAmount(Number(e.target.value))}
|
||||||
|
children={zapOptions()}
|
||||||
|
size={zapOptions().length}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
onChange={e => setZapComment(e.target.value)}
|
||||||
|
placeholder={intl.formatMessage(messages.zap_commentPlaceholder)}
|
||||||
|
/>
|
||||||
|
<Button type='submit' theme='primary' icon={require('@tabler/icons/outline/bolt.svg')} text={intl.formatMessage(messages.zap_button)} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ZapPayRequestForm;
|
|
@ -1639,5 +1639,9 @@
|
||||||
"video.pause": "Pause",
|
"video.pause": "Pause",
|
||||||
"video.play": "Play",
|
"video.play": "Play",
|
||||||
"video.unmute": "Unmute sound",
|
"video.unmute": "Unmute sound",
|
||||||
"who_to_follow.title": "People To Follow"
|
"who_to_follow.title": "People To Follow",
|
||||||
|
"zap.comment_input.placeholder": "Optional comment",
|
||||||
|
"zap.open_wallet": "Open Wallet",
|
||||||
|
"zap.send_to": "Send zaps to {target}",
|
||||||
|
"zap.unit": "Zap amount in sats"
|
||||||
}
|
}
|
Loading…
Reference in New Issue