chore: ghost button (#596)

This commit is contained in:
Anthony Potdevin 2023-12-21 12:39:20 +01:00 committed by GitHub
parent e0498a6c91
commit 5aad9670cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 407 additions and 17 deletions

View File

@ -52,6 +52,7 @@ type AmbossSubscription {
type AmbossUser {
backups: UserBackupInfo!
ghost: UserGhostInfo!
subscription: AmbossSubscription!
}
@ -263,6 +264,10 @@ type ChannelsTimeHealth {
score: Float
}
type ClaimGhostAddress {
username: String!
}
type ClosedChannel {
capacity: Float!
channel_age: Float
@ -485,6 +490,7 @@ type Mutation {
addPeer(isTemporary: Boolean, publicKey: String, socket: String, url: String): Boolean!
bosRebalance(avoid: [String!], in_through: String, max_fee: Float, max_fee_rate: Float, max_rebalance: Float, node: String, out_inbound: Float, out_through: String, timeout_minutes: Float): BosRebalanceResult!
claimBoltzTransaction(destination: String!, fee: Float!, preimage: String!, privateKey: String!, redeem: String!, transaction: String!): String!
claimGhostAddress(address: String): ClaimGhostAddress!
closeChannel(forceClose: Boolean, id: String!, targetConfirmations: Float, tokensPerVByte: Float): OpenOrCloseChannel!
createAddress(type: String! = "p2tr"): String!
createBaseInvoice(amount: Float!): BaseInvoice!
@ -843,6 +849,10 @@ type UserBackupInfo {
total_size_saved: String!
}
type UserGhostInfo {
username: String
}
type Utxo {
address: String!
address_format: String!

View File

@ -9,6 +9,7 @@ import { SectionTitle, Text } from '../../src/components/typography/Styled';
import { Healthchecks } from '../../src/views/amboss/Healthchecks';
import { Balances } from '../../src/views/amboss/Balances';
import { Billboard } from '../../src/views/amboss/Billboard';
import { Ghost } from '../../src/views/amboss/Ghost';
const AmbossView = () => (
<>
@ -28,6 +29,7 @@ const AmbossView = () => (
const Wrapped = () => (
<GridWrapper>
<AmbossView />
<Ghost />
<Backups />
<Healthchecks />
<Balances />

View File

@ -73,7 +73,7 @@ export const Link: React.FC<LinkProps> = ({
<CorrectLink
href={href}
{...props}
{...(newTab && { target: '_blank', rel: 'noreferrer' })}
{...(newTab && { target: '_blank', rel: 'noreferrer noopener' })}
>
{children}
</CorrectLink>

View File

@ -0,0 +1,21 @@
import { forwardRef } from 'react';
export const GhostLogo = forwardRef<any, any>(
({ color = 'currentColor', size = 100, children, ...rest }, ref) => {
return (
<svg
ref={ref}
xmlns="http://www.w3.org/2000/svg"
width={size}
fill={color}
viewBox="0 0 100 100"
{...rest}
>
{children}
<path d="M50 1C22.9 1 1 22.9 1 50v36.8C1 93.5 6.5 99 13.2 99s12.3-5.5 12.3-12.3C25.5 93.5 31 99 37.7 99 44.5 99 50 93.5 50 86.8 50 93.5 55.5 99 62.3 99c6.8 0 12.3-5.5 12.3-12.3C74.5 93.5 80 99 86.8 99 93.5 99 99 93.5 99 86.8V50C99 22.9 77.1 1 50 1z" />
</svg>
);
}
);
GhostLogo.displayName = 'GhostLogo';

View File

@ -0,0 +1,64 @@
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ClaimGhostAddressMutationVariables = Types.Exact<{
address?: Types.InputMaybe<Types.Scalars['String']['input']>;
}>;
export type ClaimGhostAddressMutation = {
__typename?: 'Mutation';
claimGhostAddress: { __typename?: 'ClaimGhostAddress'; username: string };
};
export const ClaimGhostAddressDocument = gql`
mutation ClaimGhostAddress($address: String) {
claimGhostAddress(address: $address) {
username
}
}
`;
export type ClaimGhostAddressMutationFn = Apollo.MutationFunction<
ClaimGhostAddressMutation,
ClaimGhostAddressMutationVariables
>;
/**
* __useClaimGhostAddressMutation__
*
* To run a mutation, you first call `useClaimGhostAddressMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useClaimGhostAddressMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [claimGhostAddressMutation, { data, loading, error }] = useClaimGhostAddressMutation({
* variables: {
* address: // value for 'address'
* },
* });
*/
export function useClaimGhostAddressMutation(
baseOptions?: Apollo.MutationHookOptions<
ClaimGhostAddressMutation,
ClaimGhostAddressMutationVariables
>
) {
const options = { ...defaultOptions, ...baseOptions };
return Apollo.useMutation<
ClaimGhostAddressMutation,
ClaimGhostAddressMutationVariables
>(ClaimGhostAddressDocument, options);
}
export type ClaimGhostAddressMutationHookResult = ReturnType<
typeof useClaimGhostAddressMutation
>;
export type ClaimGhostAddressMutationResult =
Apollo.MutationResult<ClaimGhostAddressMutation>;
export type ClaimGhostAddressMutationOptions = Apollo.BaseMutationOptions<
ClaimGhostAddressMutation,
ClaimGhostAddressMutationVariables
>;

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const CLAIM_GHOST_ADDRESS = gql`
mutation ClaimGhostAddress($address: String) {
claimGhostAddress(address: $address) {
username
}
}
`;

View File

@ -23,6 +23,7 @@ export type GetAmbossUserQuery = {
available_size: string;
remaining_size: string;
};
ghost: { __typename?: 'UserGhostInfo'; username?: string | null };
} | null;
};
@ -41,6 +42,9 @@ export const GetAmbossUserDocument = gql`
available_size
remaining_size
}
ghost {
username
}
}
}
`;

View File

@ -15,6 +15,9 @@ export const GET_AMBOSS_USER = gql`
available_size
remaining_size
}
ghost {
username
}
}
}
`;

View File

@ -83,6 +83,7 @@ export type AmbossSubscription = {
export type AmbossUser = {
__typename?: 'AmbossUser';
backups: UserBackupInfo;
ghost: UserGhostInfo;
subscription: AmbossSubscription;
};
@ -321,6 +322,11 @@ export type ChannelsTimeHealth = {
score?: Maybe<Scalars['Float']['output']>;
};
export type ClaimGhostAddress = {
__typename?: 'ClaimGhostAddress';
username: Scalars['String']['output'];
};
export type ClosedChannel = {
__typename?: 'ClosedChannel';
capacity: Scalars['Float']['output'];
@ -565,6 +571,7 @@ export type Mutation = {
addPeer: Scalars['Boolean']['output'];
bosRebalance: BosRebalanceResult;
claimBoltzTransaction: Scalars['String']['output'];
claimGhostAddress: ClaimGhostAddress;
closeChannel: OpenOrCloseChannel;
createAddress: Scalars['String']['output'];
createBaseInvoice: BaseInvoice;
@ -627,6 +634,10 @@ export type MutationClaimBoltzTransactionArgs = {
transaction: Scalars['String']['input'];
};
export type MutationClaimGhostAddressArgs = {
address?: InputMaybe<Scalars['String']['input']>;
};
export type MutationCloseChannelArgs = {
forceClose?: InputMaybe<Scalars['Boolean']['input']>;
id: Scalars['String']['input'];
@ -1199,6 +1210,11 @@ export type UserBackupInfo = {
total_size_saved: Scalars['String']['output'];
};
export type UserGhostInfo = {
__typename?: 'UserGhostInfo';
username?: Maybe<Scalars['String']['output']>;
};
export type Utxo = {
__typename?: 'Utxo';
address: Scalars['String']['output'];

View File

@ -1,8 +1,11 @@
import { createGlobalStyle } from 'styled-components';
import { backgroundColor, textColor } from './Themes';
import { Inter } from 'next/font/google';
import { Noto_Sans } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
const notoSans = Noto_Sans({
weight: ['100', '200', '300', '400', '500', '600', '700', '800', '900'],
subsets: ['latin'],
});
export const GlobalStyles = createGlobalStyle`
html, body {
@ -11,7 +14,7 @@ export const GlobalStyles = createGlobalStyle`
}
* {
font-variant-numeric: tabular-nums;
font-family: ${inter.style.fontFamily}, sans-serif;
font-family: ${notoSans.style.fontFamily}, sans-serif;
}
*, *::after, *::before {
box-sizing: border-box;
@ -20,7 +23,7 @@ export const GlobalStyles = createGlobalStyle`
background: ${backgroundColor};
color: ${textColor};
font-variant-numeric: tabular-nums;
font-family: ${inter.style.fontFamily}, sans-serif;
font-family: ${notoSans.style.fontFamily}, sans-serif;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;

View File

@ -0,0 +1,137 @@
import styled from 'styled-components';
import { ColorButton } from '../../components/buttons/colorButton/ColorButton';
import {
Card,
CardWithTitle,
Separation,
SingleLine,
SubTitle,
} from '../../components/generic/Styled';
import { Link } from '../../components/link/Link';
import { Text } from '../../components/typography/Styled';
import { useAmbossUser } from '../../hooks/UseAmbossUser';
import { AmbossLoginButton } from './LoginButton';
import { mediaWidths } from '../../styles/Themes';
import { useClaimGhostAddressMutation } from '../../graphql/mutations/__generated__/claimGhostAddress.generated';
import { toast } from 'react-toastify';
import { getErrorContent } from '../../utils/error';
import { renderLine } from '../../components/generic/helpers';
import { useState } from 'react';
import { Input } from '../../components/input';
const S = {
row: styled.div`
display: flex;
gap: 8px;
@media (${mediaWidths.mobile}) {
flex-direction: column;
}
`,
};
const Buttons = () => {
const { user } = useAmbossUser();
const isSubscribed = !!user?.subscription.subscribed;
const hasClaimedAddress = !!user?.ghost.username;
const [savedUsername, setUsername] = useState(user?.ghost.username || '');
const [claimAddress, { loading }] = useClaimGhostAddressMutation({
onCompleted: () => toast.success('Address claimed'),
onError: error => toast.error(getErrorContent(error)),
refetchQueries: ['GetAmbossUser'],
});
if (!user) {
return (
<SingleLine>
<Text style={{ margin: '0' }}>Login to claim an address</Text>
<AmbossLoginButton />
</SingleLine>
);
}
if (!isSubscribed) {
if (!!hasClaimedAddress) {
return (
<ColorButton fullWidth loading={loading}>
<Link href="https://amboss.space/pricing" newTab>
Subscribe to claim a custom address
</Link>
</ColorButton>
);
}
return (
<S.row>
<ColorButton fullWidth loading={loading} onClick={() => claimAddress()}>
Claim free random address
</ColorButton>
<ColorButton fullWidth loading={loading}>
<Link href="https://amboss.space/pricing" newTab>
Subscribe to claim a custom address
</Link>
</ColorButton>
</S.row>
);
}
return (
<SingleLine>
<Input
value={savedUsername}
placeholder={`Custom alias`}
onChange={e => setUsername(e.target.value)}
onEnter={() => claimAddress({ variables: { address: savedUsername } })}
/>
<ColorButton
loading={loading}
disabled={loading || savedUsername === '' || !savedUsername}
withMargin={'0 0 0 8px'}
onClick={() => claimAddress({ variables: { address: savedUsername } })}
>
Claim
</ColorButton>
</SingleLine>
);
};
const CurrentAddress = () => {
const { user } = useAmbossUser();
if (!user) return null;
return (
<>
<Separation />
{renderLine(
'Your Ghost address',
user.ghost.username
? `${user.ghost.username}@ghst.to`
: `Has not been claimed!`
)}
</>
);
};
export const Ghost = () => {
return (
<CardWithTitle>
<SubTitle>Ghost Address</SubTitle>
<Card>
<Text>
With a Ghost address you can have your very own
<Link href="https://lightningaddress.com/" newTab>
{' Lightning Address '}
</Link>
for you to receive payments directly to your node.
</Text>
<CurrentAddress />
<Separation />
<Buttons />
</Card>
</CardWithTitle>
);
};

View File

@ -22,6 +22,7 @@ import { LnUrlCard } from './lnurl';
import { LnMarketsCard } from './lnmarkets';
import { AmbossCard } from './amboss/AmbossCard';
import { LightningAddressCard } from './lightningAddress/LightningAddress';
import { GhostCard } from './ghost/GhostQuickAction';
export const QuickCard = styled.div`
background: ${cardColor};
@ -34,9 +35,7 @@ export const QuickCard = styled.div`
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 25px;
padding: 10px;
margin-right: 10px;
cursor: pointer;
color: #69c0ff;
@ -61,6 +60,8 @@ export const QuickTitle = styled.div`
const QuickRow = styled.div`
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 16px 0 32px;
`;
export const QuickActions = () => {
@ -104,6 +105,7 @@ export const QuickActions = () => {
default:
return (
<QuickRow>
<GhostCard />
<SupportCard callback={() => setOpenCard('support')} />
<AmbossCard />
<QuickCard onClick={() => setOpenCard('lightning_address')}>

View File

@ -30,9 +30,7 @@ const QuickCard = styled.button`
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 25px;
padding: 10px;
margin-right: 10px;
cursor: pointer;
color: #69c0ff;

View File

@ -26,9 +26,7 @@ const QuickCard = styled.div`
flex-direction: column;
justify-content: center;
align-items: center;
margin-bottom: 25px;
padding: 10px;
margin-right: 10px;
cursor: pointer;
color: #69c0ff;

View File

@ -20,6 +20,7 @@ import { useCreateThunderPointsMutation } from '../../../../graphql/mutations/__
import { toast } from 'react-toastify';
import { useBaseConnect } from '../../../../hooks/UseBaseConnect';
import { Pay } from '../../account/pay/Pay';
import { getErrorContent } from '../../../../utils/error';
const StyledText = styled.div`
text-align: center;
@ -45,7 +46,9 @@ export const SupportBar = () => {
const [withPoints, setWithPoints] = React.useState<boolean>(false);
const [getInvoice, { data, loading }] = useCreateBaseInvoiceMutation();
const [getInvoice, { data, loading }] = useCreateBaseInvoiceMutation({
onError: error => toast.error(getErrorContent(error)),
});
const [createPoints, { data: pointsData, called, loading: pointsLoading }] =
useCreateThunderPointsMutation({ refetchQueries: ['GetBasePoints'] });

View File

@ -0,0 +1,57 @@
import styled from 'styled-components';
import {
cardBorderColor,
cardColor,
mediaWidths,
unSelectedNavButton,
} from '../../../../styles/Themes';
import { GhostLogo } from '../../../../components/logo/GhostIcon';
import { useRouter } from 'next/router';
const QuickTitle = styled.div`
font-size: 12px;
color: ${unSelectedNavButton};
margin-top: 10px;
`;
const QuickCard = styled.div`
background: ${cardColor};
box-shadow: 0 8px 16px -8px rgba(0, 0, 0, 0.1);
border-radius: 4px;
border: 1px solid ${cardBorderColor};
height: 100px;
width: 100px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 10px;
cursor: pointer;
color: #69c0ff;
@media (${mediaWidths.mobile}) {
padding: 4px;
height: 80px;
width: 80px;
}
&:hover {
background-color: black;
color: white;
& ${QuickTitle} {
color: white;
}
}
`;
export const GhostCard = () => {
const { push } = useRouter();
return (
<QuickCard onClick={() => push('/amboss')}>
<GhostLogo size={24} />
<QuickTitle>Ghost</QuickTitle>
</QuickCard>
);
};

View File

@ -15,6 +15,9 @@ export const getUserQuery = gql`
remaining_size
total_size_saved
}
ghost {
username
}
}
}
`;
@ -138,3 +141,11 @@ export const getGhostPayment = gql`
}
}
`;
export const claimGhostAddress = gql`
mutation claimGhostAddress($address: String) {
claimGhostAddress(address: $address) {
username
}
}
`;

View File

@ -8,6 +8,7 @@ import { Logger } from 'winston';
import cookie from 'cookie';
import {
AmbossUser,
ClaimGhostAddress,
LightningAddress,
LightningNodeSocialInfo,
} from './amboss.types';
@ -17,6 +18,7 @@ import { NodeService } from '../../node/node.service';
import { UserId } from '../../security/security.types';
import { CurrentUser } from '../../security/security.decorators';
import {
claimGhostAddress,
getLightningAddresses,
getLoginTokenQuery,
getNodeSocialInfo,
@ -25,6 +27,7 @@ import {
loginMutation,
} from './amboss.gql';
import { AmbossService } from './amboss.service';
import { GraphQLError } from 'graphql';
const ONE_MONTH_SECONDS = 60 * 60 * 24 * 30;
@ -198,4 +201,29 @@ export class AmbossResolver {
return true;
}
@Mutation(() => ClaimGhostAddress)
async claimGhostAddress(
@Args('address', { nullable: true }) address: string | null,
@Context() { ambossAuth }: ContextType
) {
if (!ambossAuth) {
throw new GraphQLError(
'You need to login to Amboss before you can claim your Ghost address.'
);
}
const { data, error } = await this.fetchService.graphqlFetchWithProxy(
this.configService.get('urls.amboss'),
claimGhostAddress,
{ address },
{ authorization: `Bearer ${ambossAuth}` }
);
if (!data?.claimGhostAddress || error) {
throw new GraphQLError('Error claiming Ghost address.');
}
return data.claimGhostAddress;
}
}

View File

@ -28,6 +28,12 @@ export class UserBackupInfo {
remaining_size: string;
}
@ObjectType()
export class UserGhostInfo {
@Field({ nullable: true })
username: string;
}
@ObjectType()
export class AmbossUser {
@Field(() => AmbossSubscription)
@ -35,6 +41,9 @@ export class AmbossUser {
@Field(() => UserBackupInfo)
backups: UserBackupInfo;
@Field(() => UserGhostInfo)
ghost: UserGhostInfo;
}
@ObjectType()
@ -73,6 +82,12 @@ export class LightningNodeSocialInfo {
socials: NodeSocial;
}
@ObjectType()
export class ClaimGhostAddress {
@Field()
username: string;
}
export type NodeAlias = {
alias: string;
pub_key: string;

View File

@ -3,6 +3,10 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { gql } from 'graphql-tag';
import { FetchService } from '../../fetch/fetch.service';
import { BaseInvoice, BaseNode, BasePoints } from './base.types';
import { GraphQLError } from 'graphql';
import { Logger } from 'winston';
import { Inject } from '@nestjs/common';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
const getBaseCanConnectQuery = gql`
{
@ -54,7 +58,8 @@ const createThunderPointsQuery = gql`
export class BaseResolver {
constructor(
private configService: ConfigService,
private fetchService: FetchService
private fetchService: FetchService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger
) {}
@Query(() => Boolean)
@ -97,7 +102,9 @@ export class BaseResolver {
@Mutation(() => BaseInvoice)
async createBaseInvoice(@Args('amount') amount: number) {
if (!amount) return '';
if (!amount) {
throw new GraphQLError('No amount provided for donation invoice.');
}
const { data, error } = await this.fetchService.graphqlFetchWithProxy(
this.configService.get('urls.tbase'),
@ -105,10 +112,12 @@ export class BaseResolver {
{ amount }
);
if (error) return null;
if (data?.createInvoice) return data.createInvoice;
if (error || !data?.createInvoice) {
this.logger.error('Error getting donation invoice.', { error, data });
throw new GraphQLError('Error creating donation invoice.');
}
return null;
return data.createInvoice;
}
@Mutation(() => Boolean)