Skip to content

Commit 60ff6c2

Browse files
committed
feat: add views and dialogs to register a new account handle
1 parent ce50231 commit 60ff6c2

File tree

9 files changed

+219
-0
lines changed

9 files changed

+219
-0
lines changed

packages/frontend-main/src/App.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import FollowDialog from './components/popups/FollowUserDialog.vue';
88
import InvalidDefaultAmountDialog from './components/popups/InvalidDefaultAmountDialog.vue';
99
import LikePostDialog from './components/popups/LikePostDialog.vue';
1010
import NewPostDialog from './components/popups/NewPostDialog.vue';
11+
import RegisterHandleDialog from './components/popups/RegisterHandleDialog.vue';
1112
import ReplyDialog from './components/popups/ReplyDialog.vue';
1213
import TipUserDialog from './components/popups/TipUserDialog.vue';
1314
import UnfollowDialog from './components/popups/UnfollowUserDialog.vue';
@@ -43,6 +44,7 @@ onMounted(() => {
4344
<UnfollowDialog />
4445
<Sonner close-button expand dismissible :visible-toasts="5" />
4546
<TipUserDialog />
47+
<RegisterHandleDialog />
4648
<ConfirmDialog />
4749
<InvalidDefaultAmountDialog />
4850
</template>
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<script lang="ts" setup>
2+
import { Decimal } from '@cosmjs/math';
3+
import { computed, ref } from 'vue';
4+
import { toast } from 'vue-sonner';
5+
6+
import { Button } from '@/components/ui/button';
7+
import { Dialog, DialogDescription, DialogTitle, ResponsiveDialogContent } from '@/components/ui/dialog';
8+
import InputPhoton from '@/components/ui/input/InputPhoton.vue';
9+
import { useRegisterHandle } from '@/composables/useRegisterHandle';
10+
import { useTxDialog } from '@/composables/useTxDialog';
11+
import { useConfigStore } from '@/stores/useConfigStore';
12+
import { fractionalDigits } from '@/utility/atomics';
13+
import { showBroadcastingToast } from '@/utility/toast';
14+
15+
const isBalanceInputValid = ref(false);
16+
17+
const { registerHandle, txError, txSuccess } = useRegisterHandle();
18+
const { isShown, inputPhotonModel, handleClose, popupState: register } = useTxDialog<string>('registerHandle', txSuccess, txError);
19+
const configStore = useConfigStore();
20+
21+
const amountAtomics = computed(() => configStore.config.defaultAmountEnabled ? configStore.config.defaultAmountAtomics : Decimal.fromUserInput(inputPhotonModel.value.toString(), fractionalDigits).atomics);
22+
const canSubmit = computed(() => isBalanceInputValid.value);
23+
24+
function handleInputValidity(value: boolean) {
25+
isBalanceInputValid.value = value;
26+
}
27+
28+
async function handleSubmit() {
29+
if (!canSubmit.value || !register.value) {
30+
return;
31+
}
32+
33+
const handle = ref(register.value);
34+
handleClose();
35+
36+
const toastId = showBroadcastingToast('Register Handle');
37+
try {
38+
await registerHandle({ handle: handle.value, amountAtomics: amountAtomics.value });
39+
} finally {
40+
toast.dismiss(toastId);
41+
}
42+
}
43+
</script>
44+
45+
<template>
46+
<Dialog v-if="isShown" :open="isShown" @update:open="handleClose">
47+
<ResponsiveDialogContent>
48+
<DialogTitle>{{ $t('components.PopupTitles.registerHandle') }}</DialogTitle>
49+
<DialogDescription>@{{ register }}</DialogDescription>
50+
51+
<div class="flex flex-col w-full gap-4">
52+
<InputPhoton
53+
v-if="!configStore.config.defaultAmountEnabled"
54+
v-model="inputPhotonModel"
55+
@on-validity-change="handleInputValidity"
56+
/>
57+
<Button class="w-full" :disabled="!isBalanceInputValid" @click="handleSubmit">
58+
{{ $t('components.Button.submit') }}
59+
</Button>
60+
</div>
61+
</ResponsiveDialogContent>
62+
</Dialog>
63+
</template>

packages/frontend-main/src/composables/usePopups.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface PopupState {
1111
follow: string | null;
1212
unfollow: string | null;
1313
tipUser: string | null;
14+
registerHandle: string | null;
1415
invalidDefaultAmount: string | null;
1516
}
1617

@@ -23,6 +24,7 @@ const state = reactive<PopupState>({
2324
follow: null,
2425
unfollow: null,
2526
tipUser: null,
27+
registerHandle: null,
2628
invalidDefaultAmount: null,
2729
});
2830

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useMutation } from '@tanstack/vue-query';
2+
import { ref } from 'vue';
3+
4+
import { showInfoToast } from '@/utility/toast';
5+
6+
import { useTxNotification } from './useTxNotification';
7+
import { useWallet } from './useWallet';
8+
9+
interface RegisterHandleRequestMutation {
10+
handle: string;
11+
amountAtomics: string;
12+
}
13+
14+
export function useRegisterHandle() {
15+
const wallet = useWallet();
16+
const txError = ref<string>();
17+
const txSuccess = ref<string>();
18+
const isToastShown = ref(false);
19+
useTxNotification('Register Handle', txSuccess, txError);
20+
21+
const {
22+
mutateAsync,
23+
} = useMutation({
24+
mutationFn: async ({ handle, amountAtomics }: RegisterHandleRequestMutation) => {
25+
txError.value = undefined;
26+
txSuccess.value = undefined;
27+
isToastShown.value = true;
28+
29+
const result = await wallet.dither.send('RegisterHandle', {
30+
args: [handle],
31+
amount: amountAtomics,
32+
});
33+
34+
if (!result.broadcast) {
35+
txError.value = result.msg;
36+
throw new Error(result.msg);
37+
}
38+
39+
txSuccess.value = result.tx?.transactionHash;
40+
},
41+
onSuccess: () => {
42+
showInfoToast('Account Handle Registered', 'Your new handle will take effect soon', 9000);
43+
},
44+
onSettled: () => {
45+
isToastShown.value = false;
46+
},
47+
});
48+
49+
return {
50+
registerHandle: mutateAsync,
51+
txError,
52+
txSuccess,
53+
};
54+
}

packages/frontend-main/src/localization/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const messages = {
6868
follow: 'Follow User',
6969
unfollow: 'Unfollow User',
7070
tipUser: 'Tip User',
71+
registerHandle: 'Register Handle',
7172
invalidDefaultAmount: 'Not enough balance',
7273
},
7374
PopupDescriptions: {
@@ -89,6 +90,7 @@ export const messages = {
8990
settings: 'Settings',
9091
manageFollows: 'Manage Following',
9192
envConfig: 'Environment Config',
93+
account: 'Account',
9294
authz: 'Authz',
9395
post: 'Post',
9496
explore: 'Explore',
@@ -139,6 +141,9 @@ export const messages = {
139141
defaultAmount: 'Default Amount',
140142
following: 'Following',
141143
back: 'Back',
144+
accountHandle: 'Account Handle',
145+
accountHandleSummary: 'Accounts can have a linked handle name which is usually used instead of the account address.\nIf an account is linked to a handle and a new one is registered it makes the current one available to anyone.',
146+
accountHandleRegister: 'Register',
142147
whatIsIt: 'What is it?',
143148
singleSessionSummary: 'Single Session creations a local private key through a passkey to sign transactions through the Comsos Authz and Feegrant modules. The settings below let you configure the authorization.',
144149
createSession: 'Create Session',

packages/frontend-main/src/router.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router';
22

33
import { useWalletStateStore } from './stores/useWalletStateStore';
44
import AboutView from './views/AboutView.vue';
5+
import AccountView from './views/AccountView.vue';
56
import EnvConfigView from './views/EnvConfigView.vue';
67
import HomeFeedView from './views/Home/HomeFeedView.vue';
78
import HomeFollowingView from './views/Home/HomeFollowingView.vue';
@@ -23,6 +24,7 @@ export const routesNames = {
2324
profile: 'Profile',
2425
profileReplies: 'Profile Replies',
2526
settings: 'Settings',
27+
settingsAccount: 'Settings Account',
2628
settingsManageFollowers: 'Settings Manage Followers',
2729
settingsConfig: 'Settings Config',
2830
settingsSingleSession: 'Settings Single Session',
@@ -40,6 +42,7 @@ const routes = [
4042
{ path: '/profile/:address', name: routesNames.profile, component: ProfilePostsView },
4143
{ path: '/profile/:address/replies', name: routesNames.profileReplies, component: ProfileRepliesView },
4244
{ path: '/settings', name: routesNames.settings, component: SettingsView, meta: { authRequired: true } },
45+
{ path: '/settings/account', name: routesNames.settingsAccount, component: AccountView, meta: { authRequired: true } },
4346
{ path: '/settings/manage-following', name: routesNames.settingsManageFollowers, component: ManageFollowingView, meta: { authRequired: true } },
4447
{ path: '/settings/env-config', name: routesNames.settingsConfig, component: EnvConfigView, meta: { authRequired: true } },
4548
{ path: '/settings/settings-single-session', name: routesNames.settingsSingleSession, component: SettingsSingleSession, meta: { authRequired: true } },

packages/frontend-main/src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export interface DitherTypes {
1515
Dislike: [string];
1616
// PostHash
1717
Flag: [string];
18+
// Handle
19+
RegisterHandle: [string];
1820
};
1921

2022
export interface DisplayableAuthor {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<script setup lang="ts">
2+
import { Loader } from 'lucide-vue-next';
3+
import { ref, watch } from 'vue';
4+
5+
import Button from '@/components/ui/button/Button.vue';
6+
import Input from '@/components/ui/input/Input.vue';
7+
import { useAccount } from '@/composables/useAccount';
8+
import { usePopups } from '@/composables/usePopups';
9+
import { useRegisterHandle } from '@/composables/useRegisterHandle';
10+
import { useWallet } from '@/composables/useWallet';
11+
import MainLayout from '@/layouts/MainLayout.vue';
12+
import { useConfigStore } from '@/stores/useConfigStore';
13+
import { showBroadcastingToast } from '@/utility/toast';
14+
import HeaderBack from '@/views/ViewHeading.vue';
15+
16+
const wallet = useWallet();
17+
const popups = usePopups();
18+
const configStore = useConfigStore();
19+
const { data: account, isFetching } = useAccount({ address: wallet.address.value });
20+
const { registerHandle } = useRegisterHandle();
21+
22+
const handle = ref<string>(account?.value?.handle);
23+
24+
watch(account, (account) => {
25+
handle.value = account.handle;
26+
});
27+
28+
async function onClickRegister() {
29+
// TODOs:
30+
// - Handle registration click (check handle is different than current one)
31+
// - Check handle format and show error
32+
// - Check if handle is available before triggering registration
33+
34+
if (configStore.config.defaultAmountEnabled) {
35+
const toastId = showBroadcastingToast('Register Handle');
36+
try {
37+
await registerHandle({ handle: handle.value, amountAtomics: configStore.config.defaultAmountAtomics });
38+
} finally {
39+
toast.dismiss(toastId);
40+
}
41+
} else {
42+
popups.show('registerHandle', handle.value);
43+
}
44+
}
45+
</script>
46+
47+
<template>
48+
<MainLayout>
49+
<div class="flex flex-col flex-1">
50+
<HeaderBack :title="$t('components.Headings.account')" />
51+
52+
<div class="flex flex-col text-pretty">
53+
<div class="flex flex-col">
54+
<span class="pt-4 pl-4 font-bold">{{ $t(`components.Settings.accountHandle`) }}</span>
55+
<p class="p-4 text-sm">
56+
{{ $t(`components.Settings.accountHandleSummary`) }}
57+
</p>
58+
</div>
59+
60+
<div v-if="isFetching" class="flex items-center justify-center p-4 border-b">
61+
<Loader class="animate-spin " />
62+
</div>
63+
<div v-else class="flex flex-col p-4 gap-4 border-b">
64+
<div class="flex gap-2 px-8">
65+
<div class="flex h-[40px] items-center">
66+
<label class="text-sm font-semibold select-none" for="handler">@</label>
67+
</div>
68+
69+
<Input id="handle" v-model.trim="handle" class="flex-1" />
70+
71+
<Button size="sm" class="decoration-2" variant="outline" @click="onClickRegister">
72+
<span class="grow">{{ $t('components.Settings.accountHandleRegister') }}</span>
73+
</Button>
74+
</div>
75+
</div>
76+
</div>
77+
</div>
78+
</MainLayout>
79+
</template>

packages/frontend-main/src/views/SettingsView.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ import ViewHeading from './ViewHeading.vue';
1313
<ViewHeading :title="$t('components.Headings.settings')" />
1414

1515
<div class="flex flex-col">
16+
<RouterLink to="/settings/account" class="border-b">
17+
<Button size="sm" class="w-full text-left py-8" variant="ghost">
18+
<span class="grow pl-2">
19+
{{ $t('components.Headings.account') }}
20+
</span>
21+
<ChevronRight class="size-4" />
22+
</Button>
23+
</RouterLink>
24+
1625
<RouterLink to="/settings/manage-following" class="border-b">
1726
<Button size="sm" class="w-full text-left py-8" variant="ghost">
1827
<span class="grow pl-2">

0 commit comments

Comments
 (0)