Skip to content

Commit

Permalink
Merge pull request kodadot#10618 from hassnian/issue-10613
Browse files Browse the repository at this point in the history
feat: Profile creation - signuature toasts
  • Loading branch information
vikiival authored Aug 5, 2024
2 parents 13601a9 + 33a24e7 commit cc85a4f
Show file tree
Hide file tree
Showing 21 changed files with 449 additions and 225 deletions.
48 changes: 38 additions & 10 deletions components/common/Notification.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,74 @@
:duration="duration"
:title="title"
:variant="variant"
:icon="icon"
:hold-timer="holdTimer"
auto-close
show-progress-bar
@close="emit('close')"
>
<div class="flex gap-2 flex-col">
<div
class="flex gap-2"
:class="{ 'flex-col': variant !== 'success' }"
>
<p class="text-k-grey text-sm break-all">
{{ message }}
</p>

<a
v-if="action"
v-safe-href="action.url"
class="text-[16px] !text-text-color"
:class="[
variant === 'success'
? '!text-k-blue hover:!text-k-blue-hover !no-underline text-sm'
: '!text-text-color text-[16px] ',
]"
target="_blank"
>
{{ action.label }}
<NeoIcon :icon="action.icon" />
</a>
</div>
</NeoMessage>
</template>

<script lang="ts" setup>
import type { NeoMessageVariant } from '@kodadot1/brick'
import { NeoMessage } from '@kodadot1/brick'
import type {
NeoMessageIconVariant,
NeoMessageVariant } from '@kodadot1/brick'
import {
NeoIcon,
NeoMessage,
} from '@kodadot1/brick'
type NotificationAction = { label: string, url: string }
type NotificationAction = { label: string, url: string, icon?: string }
const emit = defineEmits(['close'])
withDefaults(
const props = withDefaults(
defineProps<{
title: string
message: string
title: MaybeRef<string>
message: MaybeRef<string>
duration?: number
variant?: NeoMessageVariant
action?: NotificationAction
state?: Ref<LoadingNotificationState>
variant?: MaybeRef<NeoMessageVariant>
action?: MaybeRef<NotificationAction | undefined>
holdTimer?: Ref<boolean>
icon?: Ref<NeoMessageIconVariant | undefined>
}>(),
{
variant: 'success',
duration: 10000,
action: undefined,
state: undefined,
holdTimer: undefined,
icon: undefined,
},
)
const title = computed(() => unref(props.title))
const message = computed(() => unref(props.message))
const action = computed(() => unref(props.action))
const holdTimer = computed(() => unref(props.holdTimer))
const icon = computed(() => unref(props.icon))
const variant = computed(() => unref(props.variant))
</script>
226 changes: 120 additions & 106 deletions components/profile/create/Modal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,10 @@
v-if="stage === 3"
:farcaster-user-data="farcasterUserData"
:use-farcaster="useFarcaster"
:signing-message="signingMessage"
@submit="handleFormSubmition"
@delete="handleProfileDelete"
/>
<Loading v-if="stage === 4" />
<Success
v-if="stage === 5"
@close="close"
/>
</ModalBody>
</NeoModal>
</template>
Expand All @@ -39,125 +35,46 @@
import { NeoModal } from '@kodadot1/brick'
import type { StatusAPIResponse } from '@farcaster/auth-client'
import { useDocumentVisibility } from '@vueuse/core'
import { getBioWithLinks } from '../utils'
import type {
ProfileFormData } from './stages/index'
import {
Form,
Introduction,
Loading,
Select,
Success,
} from './stages/index'
import type {
CreateProfileRequest,
SocialLink,
UpdateProfileRequest } from '@/services/profile'
import {
createProfile,
deleteProfile,
updateProfile,
uploadImage,
} from '@/services/profile'
import type { ProfileFormData } from './stages/index'
import { Form, Introduction, Select } from './stages/index'
import { deleteProfile } from '@/services/profile'
import { appClient, createChannel } from '@/services/farcaster'
type SessionState = {
state: LoadingNotificationState
error?: Error
}
const emit = defineEmits(['close', 'success', 'deleted'])
const props = defineProps<{
skipIntro?: boolean
}>()
const documentVisibility = useDocumentVisibility()
const { $i18n } = useNuxtApp()
const { urlPrefix } = usePrefix()
const { accountId } = useAuth()
const { $i18n } = useNuxtApp()
const { getSignaturePair } = useVerifyAccount()
const documentVisibility = useDocumentVisibility()
const { add: generateSession, get: getSession } = useIdMap<Ref<SessionState>>()
const { fetchProfile } = useProfile()
const { hasProfile, userProfile } = useProfile()
provide('userProfile', { hasProfile, userProfile })
const initialStep = computed(() => (props.skipIntro || hasProfile.value ? 2 : 1))
const { getSignaturePair } = useVerifyAccount()
const signingMessage = ref(false)
const vOpen = ref(true)
const stage = ref(initialStep.value)
const farcasterUserData = ref<StatusAPIResponse>()
const useFarcaster = ref(false)
const farcasterSignInIsInProgress = ref(false)
const close = () => {
vOpen.value = false
emit('close')
}
const uploadProfileImage = async (
file: File | null,
type: 'image' | 'banner',
): Promise<string | undefined> => {
if (!file) {
return undefined
}
const { signature, message } = await getSignaturePair()
const response = await uploadImage({
file,
type,
address: accountId.value,
signature,
message,
})
return response.url
}
const constructSocials = (profileData: ProfileFormData): SocialLink[] => {
return [
{
handle: profileData.farcasterHandle || '',
platform: 'Farcaster',
link: `https://warpcast.com/${profileData.farcasterHandle}`,
},
{
handle: profileData.twitterHandle || '',
platform: 'Twitter',
link: `https://twitter.com/${profileData.twitterHandle}`,
},
{
handle: profileData.website || '',
platform: 'Website',
link: profileData.website || '',
},
].filter(social => Boolean(social.handle))
}
const processProfile = async (profileData: ProfileFormData) => {
const { signature, message } = await getSignaturePair()
const imageUrl = profileData.image
? await uploadProfileImage(profileData.image, 'image')
: profileData.imagePreview
const bannerUrl = profileData.banner
? await uploadProfileImage(profileData.banner, 'banner')
: profileData.bannerPreview
const profileBody: CreateProfileRequest | UpdateProfileRequest = {
address: profileData.address,
name: profileData.name,
description: useFarcaster.value
? getBioWithLinks(profileData.description)
: profileData.description,
image: imageUrl,
banner: hasProfile.value ? bannerUrl ?? null : bannerUrl!,
socials: constructSocials(profileData),
signature,
message,
}
return hasProfile.value
? updateProfile(profileBody as UpdateProfileRequest)
: createProfile(profileBody as CreateProfileRequest)
}
const handleProfileDelete = async (address: string) => {
try {
const { signature, message } = await getSignaturePair()
Expand All @@ -176,19 +93,106 @@ const handleProfileDelete = async (address: string) => {
}
const handleFormSubmition = async (profileData: ProfileFormData) => {
stage.value = 4 // Go to loading stage
try {
await processProfile(profileData)
emit('success')
let signaturePair: undefined | SignaturePair
fetchProfile()
stage.value = 5 // Go to success stage
try {
signingMessage.value = true
signaturePair = await getSignaturePair()
signingMessage.value = false
close()
onModalAnimation(() => {
stage.value = 4 // Go to loading stage
})
}
catch (error) {
stage.value = 3 // Back to form stage
reset()
warningMessage(error!.toString())
console.error(error)
}
if (!signaturePair) {
return
}
const sessionId = generateSession(
ref({
state: 'loading',
}),
)
const session = getSession(sessionId)
if (!session) {
return
}
// using a seperate try catch to show errors using the profile creation notification
try {
showProfileCreationNotification(session)
await useUpdateProfile({
profileData,
signaturePair,
hasProfile: hasProfile.value,
useFarcaster: useFarcaster.value,
})
profileCreated(sessionId)
}
catch (error) {
profileCreationFailed(sessionId, error as Error)
}
}
const showProfileCreationNotification = (session: Ref<SessionState>) => {
const isSessionState = (state: LoadingNotificationState) =>
session.value?.state === state
loadingMessage({
title: computed(() =>
isSessionState('failed')
? $i18n.t('profiles.errors.setupFailed.title')
: $i18n.t('profiles.created'),
),
message: computed(() =>
isSessionState('failed')
? $i18n.t('profiles.errors.setupFailed.message')
: undefined,
),
state: computed(() => session?.value.state as LoadingNotificationState),
action: computed<NotificationAction | undefined>(() => {
if (isSessionState('failed')) {
return getReportIssueAction(session?.value?.error?.toString() as string)
}
if (isSessionState('succeeded')) {
return {
label: $i18n.t('viewProfile'),
icon: 'arrow-up-right',
url: `/${urlPrefix.value}/u/${accountId.value}`,
}
}
return undefined
}),
})
}
const profileCreated = (sessionId: string) => {
emit('success')
fetchProfile()
stage.value = 5 // Go to success stage
updateSession(sessionId, { state: 'succeeded' })
}
const reset = () => {
signingMessage.value = false
}
const profileCreationFailed = (sessionId: string, error: Error) => {
reset()
console.error(error)
updateSession(sessionId, { state: 'failed', error: error })
}
const onSelectFarcaster = () => {
Expand Down Expand Up @@ -252,8 +256,18 @@ const loginWithFarcaster = async () => {
farcasterUserData.value = userData.data
}
const updateSession = (id: string, newSession: SessionState) => {
const session = getSession(id)
if (!session) {
return
}
session.value = newSession
}
useModalIsOpenTracker({
isOpen: computed(() => props.modelValue),
isOpen: vOpen,
onClose: false,
onChange: () => {
stage.value = initialStep.value
Expand Down
Loading

0 comments on commit cc85a4f

Please sign in to comment.