Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TW-1614: Temple Tap Airdrop confirmation #1244

Merged
merged 12 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from 10 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
Binary file added public/misc/airdrop-btn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/misc/airdrop-popup-btn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/app/PageRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import * as Woozie from 'lib/woozie';

import { RewardsPage } from './pages/Rewards';
import { StakingPage } from './pages/Staking';
import { TempleTapAirdropPage } from './pages/TempleTapAirdrop';
import { WithDataLoading } from './WithDataLoading';

interface RouteContext {
Expand Down Expand Up @@ -100,6 +101,7 @@ const ROUTE_MAP = Woozie.createMap<RouteContext>([
['/notifications', onlyReady(() => <Notifications />)],
['/notifications/:id', onlyReady(({ id }) => <NotificationsItem id={Number(id) ?? 0} />)],
['/rewards', onlyReady(() => <RewardsPage />)],
['/temple-tap-airdrop', onlyReady(onlyInFullPage(() => <TempleTapAirdropPage />))],
['*', () => <Woozie.Redirect to="/" />]
]);

Expand Down
6 changes: 6 additions & 0 deletions src/app/icons/social-tg.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/app/icons/social-x.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 21 additions & 8 deletions src/app/layouts/PageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import ContentContainer from 'app/layouts/ContentContainer';
import { useOnboardingProgress } from 'app/pages/Onboarding/hooks/useOnboardingProgress.hook';
import { AdvertisingBanner } from 'app/templates/advertising/advertising-banner/advertising-banner';
import { AdvertisingOverlay } from 'app/templates/advertising/advertising-overlay/advertising-overlay';
import { AirdropButton } from 'app/templates/temple-tap/AirdropButton';
import { IS_MISES_BROWSER } from 'lib/env';
import { T } from 'lib/i18n';
import { NotificationsBell } from 'lib/notifications/components/bell';
Expand Down Expand Up @@ -111,21 +112,32 @@ export const SpinnerSection: FC = () => (
</div>
);

type ToolbarProps = {
interface ToolbarProps {
pageTitle?: ReactNode;
hasBackAction?: boolean;
step?: number;
setStep?: (step: number) => void;
skip?: boolean;
attention?: boolean;
};
withBell?: boolean;
withAd?: boolean;
withAirdrop?: boolean;
}

export let ToolbarElement: HTMLDivElement | null = null;

/** Defined for reference in code to highlight relation between multiple sticky elements & their sizes */
export const TOOLBAR_IS_STICKY = true;

const Toolbar: FC<ToolbarProps> = ({ pageTitle, hasBackAction = true, step, setStep, skip, attention }) => {
const Toolbar: FC<ToolbarProps> = ({
pageTitle,
hasBackAction = true,
step,
setStep,
skip,
withBell,
withAd,
withAirdrop
}) => {
const { historyPosition, pathname } = useLocation();
const { fullPage } = useAppEnv();
const { setOnboardingCompleted } = useOnboardingProgress();
Expand Down Expand Up @@ -223,10 +235,11 @@ const Toolbar: FC<ToolbarProps> = ({ pageTitle, hasBackAction = true, step, setS

<div className="flex-1" />

{attention && (
<div className="flex items-center content-end absolute right-0">
<AdvertisingBanner />
<NotificationsBell />
{(withAd || withAirdrop || withBell) && (
<div className="flex items-center gap-x-2 content-end">
{withAd && <AdvertisingBanner />}
{withAirdrop && <AirdropButton />}
{withBell && <NotificationsBell />}
</div>
)}

Expand Down
4 changes: 3 additions & 1 deletion src/app/pages/Home/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ const Home = memo<Props>(({ assetSlug }) => {
</span>
) : null
}
attention={true}
withBell
withAd
withAirdrop={!assetSlug}
>
{fullPage && (
<div className="w-full max-w-sm mx-auto">
Expand Down
Binary file added src/app/pages/TempleTapAirdrop/banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/app/pages/TempleTapAirdrop/confirmed.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
235 changes: 235 additions & 0 deletions src/app/pages/TempleTapAirdrop/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import React, { FC, memo, PropsWithChildren, useCallback, useMemo, useState } from 'react';

import { OnSubmit, useForm } from 'react-hook-form';

import { Alert, Anchor, FormField, FormSubmitButton } from 'app/atoms';
import { ReactComponent as TelegramSvg } from 'app/icons/social-tg.svg';
import { ReactComponent as XSocialSvg } from 'app/icons/social-x.svg';
import PageLayout from 'app/layouts/PageLayout';
import { makeSigAuthMessageBytes, SigAuthValues } from 'lib/apis/temple/sig-auth';
import { checkTempleTapAirdropConfirmation, sendTempleTapAirdropUsernameConfirmation } from 'lib/apis/temple-tap';
import { t } from 'lib/i18n';
import { useTypedSWR } from 'lib/swr';
import { useAccount, useTempleClient, useTezos } from 'lib/temple/front';
import { TempleAccountType } from 'lib/temple/types';
import { useLocalStorage } from 'lib/ui/local-storage';

import BannerImgSrc from './banner.png';
import { ReactComponent as ConfirmedSvg } from './confirmed.svg';

interface FormData {
username: string;
}

export const TempleTapAirdropPage = memo(() => {
const account = useAccount();
const accountPkh = account.publicKeyHash;

const tezos = useTezos();
const { silentSign } = useTempleClient();

const canSign = useMemo(
() => [TempleAccountType.HD, TempleAccountType.Imported, TempleAccountType.Ledger].includes(account.type),
[account.type]
);

const [storedRecord, setStoredRecord] = useLocalStorage<LocalStorageRecord | null>(
'TEMPLE_TAP_AIRDROP_PKH_CONFIRMATIONS',
null
);

const [confirmSent, setConfirmSent] = useState(false);
const [confirmed, setConfirmed] = useState(storedRecord?.[accountPkh] ?? false);

const prepSigAuthValues = useCallback(async () => {
const [publicKey, messageBytes] = await Promise.all([
tezos.signer.publicKey(),
makeSigAuthMessageBytes(accountPkh)
]);

const { prefixSig: signature } = await silentSign(accountPkh, messageBytes);
keshan3262 marked this conversation as resolved.
Show resolved Hide resolved

const values: SigAuthValues = { publicKey, messageBytes, signature };
alex-tsx marked this conversation as resolved.
Show resolved Hide resolved

return values;
}, [silentSign, tezos.signer, accountPkh]);

useTypedSWR(
[accountPkh],
async () => {
if (confirmed || !canSign) return;

const sigAuthValues = await prepSigAuthValues();

const confirmedRes = await checkTempleTapAirdropConfirmation(accountPkh, sigAuthValues);

if (!confirmedRes) return false;

setConfirmed(true);

return true;
},
{
suspense: true,
revalidateOnFocus: false,
refreshInterval: 60_000
}
);

const { register, handleSubmit, errors, setError, clearError, formState, reset } = useForm<FormData>();

const submitting = formState.isSubmitting;

const onSubmit = useCallback<OnSubmit<FormData>>(
async ({ username }) => {
clearError();

try {
const sigAuthValues = await prepSigAuthValues();

const res = await sendTempleTapAirdropUsernameConfirmation(accountPkh, username, sigAuthValues);

switch (res.data.status) {
case 'ACCEPTED':
setConfirmSent(true);
break;
case 'CONFIRMED':
setConfirmed(true);
setStoredRecord(state => ({ ...state, [accountPkh]: true }));
break;
}

reset();
} catch (error: any) {
console.error(error);

setError('username', 'submit-error', error?.response?.data?.message || 'Something went wrong...');
}
},
[reset, clearError, setError, setStoredRecord, prepSigAuthValues, accountPkh]
);

return (
<PageLayout pageTitle="Temple Tap Airdrop" withBell>
<div className="flex flex-col w-full max-w-sm mx-auto pb-6">
<img src={BannerImgSrc} alt="Banner" className="self-center h-28" />

<span className="mt-6 text-dark font-semibold" style={{ fontSize: 19 }}>
Airdrop criteria
</span>

<p className="mt-2 text-dark-gray text-sm">
All users who played Temple Tap are eligible to share the prize pool and receive a TKEY airdrop directly to
Tezos address.
</p>

<span className="mt-8 text-dark-gray text-base leading-tighter font-medium">How to receive TKEY?</span>

{confirmSent && (
<Alert
type="success"
title={`${t('success')} ${confirmed ? '✅' : '🛫'}`}
description="Confirmation sent to Temple Tap bot! Waiting for approve..."
autoFocus
className="mt-4"
/>
)}

{canSign ? (
confirmed ? (
<BlockComp
title="Address confirmed"
description="Your address has been successfully confirmed in Temple Tap bot for future airdrop distribution."
>
<ConfirmedSvg className="w-6 h-6 absolute top-4 right-4" />
</BlockComp>
) : (
<BlockComp
title="Confirm address"
description="Enter your telegram @username to confirm your Tezos address in Temple Tap bot for future airdrop distribution"
>
<div className="text-xs leading-5 text-dark-gray">
<span>Your address: </span>
<span>{accountPkh}</span>
</div>

<form onSubmit={handleSubmit(onSubmit)} className="contents">
<FormField
ref={register({
pattern: {
value: TG_USERNAME_REGEX,
message:
"Starts with '@'. You can use a-z, A-Z, 0-9 and '_' in between. Minimum length is 6 characters."
}
})}
name="username"
placeholder="@username"
className="mt-4"
style={{ backgroundColor: 'white' }}
disabled={submitting}
errorCaption={errors.username?.message}
/>

<FormSubmitButton type="submit" loading={submitting} className="mt-4">
Confirm
</FormSubmitButton>
</form>
</BlockComp>
)
) : (
<Alert description="Please, use a signer account to pursue." className="mt-4" />
)}

<BlockComp
title="Stay tuned for news"
description="Follow us on social media to be the first to hear about airdrop updates!"
>
<div className="h-1" />

<SocialItem title="Temple Wallet on X" IconComp={XSocialSvg} followUrl="https://x.com/TempleWallet" />

<SocialItem title="MadFish Community" IconComp={TelegramSvg} followUrl="https://t.me/MadFishCommunity" />
</BlockComp>
</div>
</PageLayout>
);
});

type LocalStorageRecord = StringRecord<true>;

interface BlockCompProps {
title: string;
description: string;
}

const BlockComp: FC<PropsWithChildren<BlockCompProps>> = ({ title, description, children }) => (
<div className="mt-4 relative flex flex-col p-4 bg-gray-100 rounded-xl">
<span className="text-sm font-semibold text-dark">{title}</span>

<p className="my-1 text-xs leading-5 text-gray-600">{description}</p>

{children}
</div>
);

interface SocialItemProps {
title: string;
IconComp: ImportedSVGComponent;
followUrl: string;
}

const SocialItem: FC<SocialItemProps> = ({ title, IconComp, followUrl }) => (
<div className="mt-2 flex items-center gap-x-3 py-3 px-4 bg-white rounded-xl">
<div className="w-10 h-10 p-3 bg-blue-150 rounded-full">
<IconComp className="w-4 h-4 stroke-current fill-current text-dark" />
</div>

<span className="flex-grow text-sm font-semibold text-dark">{title}</span>

<Anchor href={followUrl} className="p-2 text-sm leading-none font-semibold text-white bg-blue-550 rounded-lg">
Follow
</Anchor>
</div>
);

const TG_USERNAME_REGEX = /^@[a-zA-Z0-9](?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9])$/;
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const AdvertisingBanner: FC = () => {

return (
<Anchor
className="flex items-center justify-center mr-3"
className="flex items-center justify-center"
style={{
height: 28,
paddingLeft: popup ? 4 : 8,
Expand Down
21 changes: 21 additions & 0 deletions src/app/templates/temple-tap/AirdropButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { memo } from 'react';

import clsx from 'clsx';

import { useAppEnv } from 'app/env';
import { Link } from 'lib/woozie';

export const AirdropButton = memo(() => {
const { popup } = useAppEnv();

return (
<Link to="/temple-tap-airdrop" className={clsx('h-7', popup && '-mr-2')}>
<img
src={`/misc/${popup ? 'airdrop-popup-btn.png' : 'airdrop-btn.png'}`}
alt="Temple Tap Airdrop"
className="pointer-events-none"
style={{ height: 35 }}
/>
</Link>
);
});
Loading
Loading