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
143 changes: 105 additions & 38 deletions frontend/src/components/TradeBox/Prompts/Successful.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { useTranslation, Trans } from 'react-i18next';
import {
Grid,
Expand All @@ -15,36 +15,70 @@ import {
} from '@mui/material';
import currencies from '../../../../static/assets/currencies.json';
import TradeSummary from '../TradeSummary';
import { Favorite, RocketLaunch, ContentCopy, Refresh } from '@mui/icons-material';
import { Favorite, RocketLaunch, ContentCopy, Refresh, Info } from '@mui/icons-material';
import { LoadingButton } from '@mui/lab';

import { finalizeEvent, type Event } from 'nostr-tools';
import { type Order } from '../../../models';
import { systemClient } from '../../../services/System';
import {
FederationContext,
type UseFederationStoreType,
} from '../../../contexts/FederationContext';
import { type UseAppStoreType, AppContext } from '../../../contexts/AppContext';
import { GarageContext, type UseGarageStoreType } from '../../../contexts/GarageContext';

interface SuccessfulPromptProps {
order: Order;
ratePlatform: (rating: number) => void;
rateUserPlatform: (rating: number) => void;
onClickStartAgain: () => void;
onClickRenew: () => void;
loadingRenew: boolean;
}

export const SuccessfulPrompt = ({
order,
ratePlatform,
rateUserPlatform,
onClickStartAgain,
onClickRenew,
loadingRenew,
}: SuccessfulPromptProps): JSX.Element => {
const { t } = useTranslation();
const currencyCode: string = currencies[`${order.currency}`];
const { settings } = useContext<UseAppStoreType>(AppContext);
const { federation } = useContext<UseFederationStoreType>(FederationContext);
const { garage } = useContext<UseGarageStoreType>(GarageContext);

const [hostRating, setHostRating] = useState<number>();

const rateHostPlatform = function (): void {
if (!hostRating) return;

const slot = garage.getSlot();
const coordinatorPubKey = federation.getCoordinator(order.shortAlias)?.nostrHexPubkey;

if (!slot?.nostrPubKey || !slot.nostrSecKey || !coordinatorPubKey || !order.id) return;

const [rating, setRating] = useState<number | undefined>(undefined);
const eventTemplate: Event = {
kind: 31986,
created_at: Math.floor(Date.now() / 1000),
tags: [
['d', `${order.shortAlias}:${order.id}`],
['p', coordinatorPubKey],
['rating', String(hostRating / 5)],
],
content: '',
pubkey: slot.nostrPubKey,
id: '',
sig: '',
};

const signedEvent = finalizeEvent(eventTemplate, slot.nostrSecKey);
federation.roboPool.sendEvent(signedEvent);
};

useEffect(() => {
rateHostPlatform();
}, [hostRating]);

return (
<Grid
Expand All @@ -57,8 +91,8 @@ export const SuccessfulPrompt = ({
>
<Grid item xs={12}>
<Typography variant='body2' align='center'>
{t('What do you think your order host "{{coordinator}}"?', {
coordinator: federation.getCoordinator(order.shortAlias)?.longAlias,
{t('Rate your peer {{peer_nick}}', {
peer_nick: order.is_maker ? order.taker_nick : order.maker_nick,
})}
</Typography>
</Grid>
Expand All @@ -69,12 +103,36 @@ export const SuccessfulPrompt = ({
size='large'
onChange={(e) => {
const rate = e.target.value;
ratePlatform(rate);
setRating(rate);
rateUserPlatform(rate);
}}
/>
</Grid>
<Grid item xs={12}>
<Typography variant='body2' align='center'>
{t('Rate your host {{coordinator}}', {
coordinator: federation.getCoordinator(order.shortAlias)?.longAlias,
})}{' '}
<Typography variant='button' align='center'>
{t('BETA')}
</Typography>
<Tooltip title={t('You need to enable nostr to rate your coordinator.')}>
<Info sx={{ width: 15 }} />
</Tooltip>
</Typography>
</Grid>
<Grid item>
<Rating
disabled={settings.connection !== 'nostr'}
name='size-large'
defaultValue={0}
size='large'
onChange={(e) => {
const rate = e.target.value;
setHostRating(rate);
}}
/>
</Grid>
{rating === 5 ? (
{hostRating ? (
<Grid item xs={12}>
<div
style={{
Expand All @@ -84,35 +142,44 @@ export const SuccessfulPrompt = ({
justifyContent: 'center',
}}
>
{hostRating === 5 ? (
<>
<Typography variant='body2' align='center'>
<b>
{t('Thank you! {{shortAlias}} loves you too', {
shortAlias: federation.getCoordinator(order.shortAlias)?.longAlias,
})}
</b>
</Typography>
<Favorite color='error' />
</>
) : (
<Typography variant='body2' align='center'>
<b>{t('Thank you for using Robosats!')}</b>
</Typography>
)}
</div>
{hostRating === 5 ? (
<Typography variant='body2' align='center'>
<b>{t('Thank you! RoboSats loves you too')}</b>
{t(
'RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!',
)}
</Typography>
<Favorite color='error' />
</div>
<Typography variant='body2' align='center'>
{t(
'RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!',
)}
</Typography>
</Grid>
) : rating !== undefined ? (
<Grid>
<Typography variant='body2' align='center'>
<b>{t('Thank you for using Robosats!')}</b>
</Typography>
<Typography variant='body2' align='center'>
<Trans i18nKey='let_us_know_hot_to_improve'>
Let us know how the platform could improve (
<Link target='_blank' href='https://t.me/robosats'>
Telegram
</Link>
{' / '}
<Link target='_blank' href='https://github.com/RoboSats/robosats/issues'>
Github
</Link>
)
</Trans>
</Typography>
) : (
<Typography variant='body2' align='center'>
<Trans i18nKey='let_us_know_hot_to_improve'>
Let us know how the platform could improve (
<Link target='_blank' href='https://t.me/robosats'>
Telegram
</Link>
{' / '}
<Link target='_blank' href='https://github.com/RoboSats/robosats/issues'>
Github
</Link>
)
</Trans>
</Typography>
)}
</Grid>
) : (
<></>
Expand Down
9 changes: 4 additions & 5 deletions frontend/src/components/TradeBox/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useState, useEffect, useContext } from 'react';
import { Box, Divider, Grid } from '@mui/material';
import { getWebln, pn } from '../../utils';

import {
ConfirmCancelDialog,
ConfirmCollabCancelDialog,
Expand Down Expand Up @@ -309,7 +308,7 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
}
};

const ratePlatform = function (rating: number): void {
const rateUserPlatform = function (rating: number): void {
submitAction({ action: 'rate_platform', rating });
};

Expand Down Expand Up @@ -617,7 +616,7 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
return (
<SuccessfulPrompt
order={order}
ratePlatform={ratePlatform}
rateUserPlatform={rateUserPlatform}
onClickStartAgain={onStartAgain}
loadingRenew={loadingButtons.renewOrder}
onClickRenew={() => {
Expand All @@ -641,7 +640,7 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
return (
<SuccessfulPrompt
order={order}
ratePlatform={ratePlatform}
rateUserPlatform={rateUserPlatform}
onClickStartAgain={onStartAgain}
loadingRenew={loadingButtons.renewOrder}
onClickRenew={() => {
Expand Down Expand Up @@ -680,7 +679,7 @@ const TradeBox = ({ currentOrder, onStartAgain }: TradeBoxProps): JSX.Element =>
return (
<SuccessfulPrompt
order={order}
ratePlatform={ratePlatform}
rateUserPlatform={rateUserPlatform}
onClickStartAgain={onStartAgain}
loadingRenew={loadingButtons.renewOrder}
onClickRenew={() => {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/models/Coordinator.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ export class Coordinator {
this.testnet = value.testnet;
this.mainnetNodesPubkeys = value.mainnetNodesPubkeys;
this.testnetNodesPubkeys = value.testnetNodesPubkeys;
this.nostrHexPubkey = value.nostrHexPubkey;
this.url = '';
this.basePath = '';

Expand All @@ -163,6 +164,7 @@ export class Coordinator {
public testnetNodesPubkeys: string[] | undefined;
public url: string;
public basePath: string;
public nostrHexPubkey: string;

// These properties are fetched from coordinator API
public book: Record<string, PublicOrder> = {};
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/models/Robot.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class Robot {
public token?: string;
public pubKey?: string;
public encPrivKey?: string;
public nostrPubKey?: string;
public stealthInvoices: boolean = true;
public activeOrderId?: number;
public lastOrderId?: number;
Expand All @@ -34,9 +35,11 @@ class Robot {
const tokenSHA256 = this.tokenSHA256 ?? '';
const encPrivKey = this.encPrivKey ?? '';
const pubKey = this.pubKey ?? '';
const nostrPubKey = this.nostrPubKey ?? '';

return {
tokenSHA256,
nostrPubKey,
keys: {
pubKey: pubKey.split('\n').join('\\'),
encPrivKey: encPrivKey.split('\n').join('\\'),
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/models/Slot.model.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { sha256 } from 'js-sha256';
import { sha256 as sha256Hash } from '@noble/hashes/sha256';
import { Robot, Order, type Federation } from '.';
import { roboidentitiesClient } from '../services/Roboidentities/Web';
import { hexToBase91, validateTokenEntropy } from '../utils';
import { getPublicKey } from 'nostr-tools';

export interface AuthHeaders {
tokenSHA256: string;
nostrPubKey: string;
keys: {
pubKey: string;
encPrivKey: string;
Expand Down Expand Up @@ -33,6 +36,11 @@ class Slot {
const { hasEnoughEntropy, bitsEntropy, shannonEntropy } = validateTokenEntropy(token);
const tokenSHA256 = hexToBase91(sha256(token));

const nostrHash = sha256Hash(this.token);
this.nostrSecKey = new Uint8Array(nostrHash);
const nostrPubKey = getPublicKey(this.nostrSecKey);
this.nostrPubKey = nostrPubKey;

this.robots = shortAliases.reduce((acc: Record<string, Robot>, shortAlias: string) => {
acc[shortAlias] = new Robot({
...robotAttributes,
Expand All @@ -41,6 +49,7 @@ class Slot {
bitsEntropy,
shannonEntropy,
tokenSHA256,
nostrPubKey,
});
this.updateSlotFromRobot(acc[shortAlias]);
return acc;
Expand All @@ -57,6 +66,8 @@ class Slot {
activeOrder: Order | null = null;
lastOrder: Order | null = null;
copiedToken: boolean;
nostrSecKey?: Uint8Array;
nostrPubKey?: string;

onSlotUpdate: () => void;

Expand Down
6 changes: 6 additions & 0 deletions frontend/src/services/RoboPool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ class RoboPool {
this.sendMessage(JSON.stringify(requestPending));
this.sendMessage(JSON.stringify(requestSuccess));
};

sendEvent = (event: Event): void => {
const message = ['EVENT', event];

this.sendMessage(JSON.stringify(message));
};
}

export default RoboPool;
2 changes: 2 additions & 0 deletions frontend/src/services/api/ApiNativeClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ class ApiNativeClient implements ApiClient {
...headers,
...{
Authorization: `Token ${auth.tokenSHA256}`,
Nostr: auth.nostrPubKey,
},
};
} else if (auth?.keys != null) {
headers = {
...headers,
...{
Authorization: `Token ${auth.tokenSHA256} | Public ${auth.keys.pubKey} | Private ${auth.keys.encPrivKey}`,
Nostr: auth.nostrPubKey,
},
};
}
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/services/api/ApiWebClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ class ApiWebClient implements ApiClient {
...headers,
...{
Authorization: `Token ${auth.tokenSHA256}`,
Nostr: auth.nostrPubKey,
},
};
} else if (auth?.keys != null) {
headers = {
...headers,
...{
Authorization: `Token ${auth.tokenSHA256} | Public ${auth.keys.pubKey} | Private ${auth.keys.encPrivKey}`,
Nostr: auth.nostrPubKey,
},
};
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/services/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ApiNativeClient from './ApiNativeClient';

export interface Auth {
tokenSHA256: string;
nostrPubKey: string;
keys?: { pubKey: string; encPrivKey: string };
}

Expand Down
7 changes: 5 additions & 2 deletions frontend/static/locales/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -675,13 +675,16 @@
"#81": "Phrases in components/TradeBox/Prompts/SendingSats.tsx",
"RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must be online in order to receive payments.": "RoboSats està intentant pagar la teva factura de Lightning. Recorda que els nodes Lightning han d'estar en línia per rebre pagaments.",
"#82": "Phrases in components/TradeBox/Prompts/Successful.tsx",
"BETA": "BETA",
"Rate your host {{coordinator}}": "Rate your host {{coordinator}}",
"Rate your peer {{peer_nick}}": "Rate your peer {{peer_nick}}",
"Renew": "Renovar",
"RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!": "RoboSats millora amb més usuaris i liquiditat. Ensenya-li RoboSats a un amic bitcoiner!",
"Sending coins to": "Enviant monedes a",
"Start Again": "Començar de nou",
"Thank you for using Robosats!": "Gràcies per fer servir RoboSats!",
"Thank you! RoboSats loves you too": "Gràcies! RoboSats també t'estima",
"What do you think your order host \"{{coordinator}}\"?": "Què en penses del teu amfitrió \"{{coordinator}}\"?",
"Thank you! {{shortAlias}} loves you too": "Thank you! {{shortAlias}} loves you too",
"You need to enable nostr to rate your coordinator.": "You need to enable nostr to rate your coordinator.",
"Your TXID": "El teu TXID",
"#83": "Phrases in components/TradeBox/Prompts/TakerFound.tsx",
"Please wait for the taker to lock a bond. If the taker does not lock a bond in time, the order will be made public again.": "Si us plau, espera a que el prenedor bloquegi la seva fiança. Si no ho fa a temps, l'ordre serà pública de nou.",
Expand Down
Loading