diff --git a/components/common/Notification.vue b/components/common/Notification.vue
index ac0e475cd0..39244abef5 100644
--- a/components/common/Notification.vue
+++ b/components/common/Notification.vue
@@ -3,11 +3,16 @@
:duration="duration"
:title="title"
:variant="variant"
+ :icon="icon"
+ :hold-timer="holdTimer"
auto-close
show-progress-bar
@close="emit('close')"
>
-
+
diff --git a/components/profile/create/Modal.vue b/components/profile/create/Modal.vue
index 8676f6b47c..0530153a8d 100644
--- a/components/profile/create/Modal.vue
+++ b/components/profile/create/Modal.vue
@@ -23,14 +23,10 @@
v-if="stage === 3"
:farcaster-user-data="farcasterUserData"
:use-farcaster="useFarcaster"
+ :signing-message="signingMessage"
@submit="handleFormSubmition"
@delete="handleProfileDelete"
/>
-
-
@@ -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
[>()
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()
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 => {
- 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()
@@ -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) => {
+ 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(() => {
+ 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 = () => {
@@ -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
diff --git a/components/profile/create/stages/Form.vue b/components/profile/create/stages/Form.vue
index f69804fa44..68889cf8b4 100644
--- a/components/profile/create/stages/Form.vue
+++ b/components/profile/create/stages/Form.vue
@@ -131,7 +131,11 @@
()
const deleteConfirm = ref()
@@ -272,7 +277,11 @@ const form = reactive({
const userProfile = computed(() => profile?.userProfile.value)
const missingImage = computed(() => (form.imagePreview ? false : !form.image))
const submitDisabled = computed(
- () => !form.name || !form.description || missingImage.value,
+ () =>
+ !form.name
+ || !form.description
+ || missingImage.value
+ || props.signingMessage,
)
const validatingFormInput = (model: string) => {
diff --git a/components/profile/create/stages/Loading.vue b/components/profile/create/stages/Loading.vue
deleted file mode 100644
index d3ef6d2917..0000000000
--- a/components/profile/create/stages/Loading.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-
- ]
-
-
-
- No signature needed
-
-
-
-
-
-
-
diff --git a/components/profile/create/stages/Success.vue b/components/profile/create/stages/Success.vue
deleted file mode 100644
index ee535136d8..0000000000
--- a/components/profile/create/stages/Success.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-
-
-
-
-
-
-
- {{ $t('confirmed') }}
-
-
-
-
- Profile created successfully
-
-
-
-
-
-
-
-
-
-
-
diff --git a/components/profile/create/stages/index.ts b/components/profile/create/stages/index.ts
index 9a3c35ac9d..59a3077ef7 100644
--- a/components/profile/create/stages/index.ts
+++ b/components/profile/create/stages/index.ts
@@ -1,8 +1,6 @@
export { default as Introduction } from './Introduction.vue'
export { default as Select } from './Select.vue'
export { default as Form } from './Form.vue'
-export { default as Loading } from './Loading.vue'
-export { default as Success } from './Success.vue'
export type ProfileFormData = {
address: string
diff --git a/components/profile/create/utils.ts b/components/profile/create/utils.ts
new file mode 100644
index 0000000000..b2e0c9d777
--- /dev/null
+++ b/components/profile/create/utils.ts
@@ -0,0 +1,51 @@
+import type { ProfileFormData } from './stages'
+import type { SocialLink } from '@/services/profile'
+import { uploadImage } from '@/services/profile'
+
+export 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))
+}
+
+export const uploadProfileImage = async ({
+ file,
+ type,
+ signaturePair: { signature, message },
+}: {
+ file: File | null
+ type: 'image' | 'banner'
+ signaturePair: SignaturePair
+}): Promise => {
+ if (!file) {
+ return undefined
+ }
+
+ const { accountId } = useAuth()
+
+ const response = await uploadImage({
+ file,
+ type,
+ address: accountId.value,
+ signature,
+ message,
+ })
+
+ return response.url
+}
diff --git a/components/shared/DynamicGrid.vue b/components/shared/DynamicGrid.vue
index 3add1ba5af..c3bdd404a0 100644
--- a/components/shared/DynamicGrid.vue
+++ b/components/shared/DynamicGrid.vue
@@ -2,6 +2,7 @@
() {
+ const map = ref(new Map())
+
+ const add = (initialValue: T) => {
+ const id = window.crypto.randomUUID()
+ map.value.set(id, initialValue as any)
+ return id
+ }
+
+ const get = (id: string): T | undefined => map.value.get(id) as T | undefined
+
+ return {
+ add,
+ get,
+ }
+}
diff --git a/composables/useModalIsOpenTracker.ts b/composables/useModalIsOpenTracker.ts
index e1189e5754..26842cd4b1 100644
--- a/composables/useModalIsOpenTracker.ts
+++ b/composables/useModalIsOpenTracker.ts
@@ -2,6 +2,9 @@ import { debounce } from 'lodash'
export const NEO_MODAL_ANIMATION_DURATION = 200
+export const onModalAnimation = onChange =>
+ debounce(onChange, NEO_MODAL_ANIMATION_DURATION)()
+
export default ({
onChange,
isOpen,
@@ -15,7 +18,7 @@ export default ({
}) => {
watch([isOpen, () => and], ([isOpen, and]) => {
if (!isOpen === onClose && and.every(Boolean)) {
- ;(onClose ? debounce(onChange, NEO_MODAL_ANIMATION_DURATION) : onChange)()
+ ;(onClose ? onModalAnimation(onChange) : onChange)()
}
})
}
diff --git a/composables/useUpdateProfile.ts b/composables/useUpdateProfile.ts
new file mode 100644
index 0000000000..aa636d34e1
--- /dev/null
+++ b/composables/useUpdateProfile.ts
@@ -0,0 +1,58 @@
+import type {
+ CreateProfileRequest,
+ UpdateProfileRequest } from '@/services/profile'
+import {
+ createProfile,
+ updateProfile,
+} from '@/services/profile'
+import { getBioWithLinks } from '@/components/profile/utils'
+import {
+ constructSocials,
+ uploadProfileImage,
+} from '@/components/profile/create/utils'
+import type { ProfileFormData } from '@/components/profile/create/stages'
+
+export default async ({
+ profileData,
+ signaturePair,
+ hasProfile,
+ useFarcaster,
+}: {
+ profileData: ProfileFormData
+ signaturePair: SignaturePair
+ hasProfile: boolean
+ useFarcaster: boolean
+}) => {
+ const imageUrl = profileData.image
+ ? await uploadProfileImage({
+ file: profileData.image,
+ type: 'image',
+ signaturePair,
+ })
+ : profileData.imagePreview
+
+ const bannerUrl = profileData.banner
+ ? await uploadProfileImage({
+ file: profileData.banner,
+ type: 'banner',
+ signaturePair,
+ })
+ : profileData.bannerPreview
+
+ const profileBody: CreateProfileRequest | UpdateProfileRequest = {
+ address: profileData.address,
+ name: profileData.name,
+ description: useFarcaster
+ ? getBioWithLinks(profileData.description)
+ : profileData.description,
+ image: imageUrl,
+ banner: hasProfile ? (bannerUrl ?? null) : bannerUrl!,
+ socials: constructSocials(profileData),
+ signature: signaturePair.signature,
+ message: signaturePair.message,
+ }
+
+ return hasProfile
+ ? updateProfile(profileBody as UpdateProfileRequest)
+ : createProfile(profileBody as CreateProfileRequest)
+}
diff --git a/composables/useVerifyAccount.ts b/composables/useVerifyAccount.ts
index bdd6e58817..726f23b93c 100644
--- a/composables/useVerifyAccount.ts
+++ b/composables/useVerifyAccount.ts
@@ -1,6 +1,8 @@
import { isEthereumAddress } from '@polkadot/util-crypto'
import { signMessage as signMessageEvm } from '@wagmi/core'
+export type SignaturePair = { signature: string, message: string }
+
const signMessagePolkadot = async (address: string, message: string) => {
const injector = await getAddress(toDefaultAddress(address))
const signedMessage = await injector.signer.signRaw({
@@ -51,7 +53,7 @@ export default function useVerifyAccount() {
throw new Error('You have not completed address verification')
}
- const getSignaturePair = async () => {
+ const getSignaturePair = async (): Promise => {
const signature = await getSignedMessage()
return {
signature,
diff --git a/libs/ui/src/components/NeoIcon/NeoIcon.vue b/libs/ui/src/components/NeoIcon/NeoIcon.vue
index c88b67d713..ed04d67102 100644
--- a/libs/ui/src/components/NeoIcon/NeoIcon.vue
+++ b/libs/ui/src/components/NeoIcon/NeoIcon.vue
@@ -5,6 +5,7 @@
:size="size || 'default'"
:custom-size="customSize"
:variant="variant"
+ :spin="spin"
/>
@@ -17,6 +18,7 @@ defineProps<{
pack?: string
customSize?: string
variant?: 'success' | 'primary' | 'k-grey'
+ spin?: boolean
}>()
diff --git a/libs/ui/src/components/NeoMessage/NeoMessage.scss b/libs/ui/src/components/NeoMessage/NeoMessage.scss
index 5cd2d7f36b..1a72ac7c9b 100644
--- a/libs/ui/src/components/NeoMessage/NeoMessage.scss
+++ b/libs/ui/src/components/NeoMessage/NeoMessage.scss
@@ -1,6 +1,17 @@
.message {
@apply bg-[whitesmoke] rounded-none text-base;
+ &__success {
+ @apply bg-k-green-light text-k-green;
+
+ .message-progress {
+ @apply bg-k-green;
+ }
+ }
+
+ &__neutral {
+ @apply bg-background-color text-k-grey;
+ }
&__danger {
@apply bg-k-red-accent-2 text-k-red;
diff --git a/libs/ui/src/components/NeoMessage/NeoMessage.vue b/libs/ui/src/components/NeoMessage/NeoMessage.vue
index c277b7d7c4..aef1c3d8fa 100644
--- a/libs/ui/src/components/NeoMessage/NeoMessage.vue
+++ b/libs/ui/src/components/NeoMessage/NeoMessage.vue
@@ -3,14 +3,15 @@
@@ -53,13 +54,18 @@
import { useElementHover } from '@vueuse/core'
import NeoButton from '../NeoButton/NeoButton.vue'
import NeoIcon from '../NeoIcon/NeoIcon.vue'
-import type { NeoMessageVariant } from '../../types'
+import type {
+ NeoMessageCustomIconVariant,
+ NeoMessageIconVariant,
+ NeoMessageVariant,
+} from '../../types'
-const iconVariant: Record
= {
+const iconVariant: Record = {
info: 'circle-info',
- success: 'check-circle',
+ success: 'check',
warning: 'circle-exclamation',
danger: 'circle-exclamation',
+ neutral: 'circle-info',
}
const emit = defineEmits(['close', 'update:active', 'click'])
@@ -72,6 +78,8 @@ const props = withDefaults(
autoClose: boolean
duration: number
showProgressBar: boolean
+ icon?: NeoMessageIconVariant
+ holdTimer?: boolean
}>(),
{
active: true,
@@ -81,6 +89,8 @@ const props = withDefaults(
showProgressBar: false,
variant: 'success',
title: '',
+ icon: undefined,
+ holdTimer: false,
},
)
@@ -91,7 +101,17 @@ const timer = ref()
const isActive = ref(props.active)
const remainingTime = ref(props.duration)
-const computedIcon = computed(() => iconVariant[props.variant] ?? null)
+const computedIcon = computed(
+ () => props.icon ?? iconVariant[props.variant] ?? null,
+)
+const iconName = computed(
+ () =>
+ (computedIcon.value as NeoMessageCustomIconVariant)?.icon
+ ?? computedIcon.value,
+)
+const iconSpin = computed(
+ () => (computedIcon.value as NeoMessageCustomIconVariant).spin,
+)
const percent = computed(() => {
return (remainingTime.value / props.duration) * 100
@@ -104,8 +124,8 @@ const close = () => {
emit('update:active', false)
}
-watch(isActive, (active) => {
- if (active) {
+watch([isActive, () => props.holdTimer], ([active, holdTimer]) => {
+ if (active && !holdTimer) {
startTimer()
}
else if (timer.value) {
@@ -136,7 +156,7 @@ watch(
active => (isActive.value = active),
)
-onMounted(startTimer)
+onMounted(() => !props.holdTimer && startTimer())
onUnmounted(() => clearTimeout(timer.value))
diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts
index abee52a8f5..d17b65d449 100644
--- a/libs/ui/src/index.ts
+++ b/libs/ui/src/index.ts
@@ -5,8 +5,11 @@ export { default as NeoDropdownItem } from './components/NeoDropdown/NeoDropdown
export { default as NeoSelect } from './components/NeoSelect/NeoSelect.vue'
export { default as NeoSidebar } from './components/NeoSidebar/NeoSidebar.vue'
export { default as NeoCheckbox } from './components/NeoCheckbox/NeoCheckbox.vue'
-export { type NeoButtonVariant } from './types'
-export { type NeoMessageVariant } from './types'
+export type {
+ NeoButtonVariant,
+ NeoMessageVariant,
+ NeoMessageIconVariant,
+} from './types'
export { default as TheButton } from './components/TheButton/TheButton.vue'
export { default as NeoTooltip } from './components/NeoTooltip/NeoTooltip.vue'
diff --git a/libs/ui/src/scss/tailwind.scss b/libs/ui/src/scss/tailwind.scss
index 322362b25c..5b9acc702f 100644
--- a/libs/ui/src/scss/tailwind.scss
+++ b/libs/ui/src/scss/tailwind.scss
@@ -27,6 +27,7 @@
--k-accent-light-2-dark-head: #191718;
--k-accent-light-2-dark-paragraph: #191718;
--k-green: #04af00;
+ --k-green-light: #f3fbf3;
--k-red: #ff5757;
--k-orange: #cf9a10;
--k-orange-light: #FFD379;
@@ -99,6 +100,7 @@
--k-yellow-light: #3F3500;
--k-blue-accent: #2e50a2;
--k-aqua-blue: #106153;
+ --k-green-light: #0a3009;
--k-green-accent: #056a02;
--k-green-accent-2: #0a3009;
--k-blue-light: #363234;
diff --git a/libs/ui/src/types.ts b/libs/ui/src/types.ts
index b98307f525..25521d31f9 100644
--- a/libs/ui/src/types.ts
+++ b/libs/ui/src/types.ts
@@ -15,4 +15,12 @@ export type NeoButtonVariant =
| 'pill'
| 'border-icon'
-export type NeoMessageVariant = 'warning' | 'success' | 'danger' | 'info'
+export type NeoMessageVariant =
+ | 'warning'
+ | 'success'
+ | 'danger'
+ | 'info'
+ | 'neutral'
+
+export type NeoMessageCustomIconVariant = { icon: string, spin: boolean }
+export type NeoMessageIconVariant = string | NeoMessageCustomIconVariant
diff --git a/libs/ui/tailwind.config.js b/libs/ui/tailwind.config.js
index 8798fdfc58..c5759cc8f1 100644
--- a/libs/ui/tailwind.config.js
+++ b/libs/ui/tailwind.config.js
@@ -27,6 +27,7 @@ module.exports = {
'var(--k-accent-light-2-dark-paragraph)',
'k-accent-light-3': 'var(--k-accent-light-3)',
'k-green': 'var(--k-green)',
+ 'k-green-light': 'var(--k-green-light)',
'k-red': 'var(--k-red)',
'k-orange': 'var(--k-orange)',
'k-orange-light': 'var(--k-orange-light)',
diff --git a/locales/en.json b/locales/en.json
index 1024f78bff..d2c4834811 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -2075,6 +2075,10 @@
"unsuccessfulFarcasterAuth": {
"title": "Unsuccessful Connection",
"message": "Somehting went wrong linking with your farcaster account, please try again."
+ },
+ "setupFailed": {
+ "title": "Profile Setup Failed",
+ "message": "Error occurred. Try again later or report issue."
}
},
@@ -2087,7 +2091,9 @@
"deleteConfirm": "You sure? - click again",
"waitSeconds": "Wait {0} Seconds",
"profileReset": "Profile Reset",
- "profileHasBeenCleared": "Your profile has been cleared successfully. Start fresh!"
+ "profileHasBeenCleared": "Your profile has been cleared successfully. Start fresh!",
+ "finishCustomization": "Finish Customization",
+ "created": "Profile Created"
},
"reconnect": {
"required": "Reconnect Required",
diff --git a/utils/notification.ts b/utils/notification.ts
index 22e8c5ae26..9b1d2da5cf 100644
--- a/utils/notification.ts
+++ b/utils/notification.ts
@@ -1,4 +1,5 @@
import {
+ type NeoMessageIconVariant,
type NeoMessageVariant,
NeoNotificationProgrammatic as Notif,
} from '@kodadot1/brick'
@@ -7,7 +8,7 @@ import { h } from 'vue'
import Notification from '@/components/common/Notification.vue'
import MessageNotify from '@/components/MessageNotify.vue'
-type NotificationAction = { label: string, url: string }
+export type NotificationAction = { label: string, url: string }
type Params = {
variant: NeoMessageVariant
@@ -34,14 +35,20 @@ export const showNotification = ({
title,
message,
action,
+ variant,
+ holdTimer,
+ icon,
params = notificationTypes.info,
duration = 10000,
}: {
- title: string
- message: string | null
+ title?: MaybeRef
+ message?: MaybeRef | null
+ variant?: Ref
params?: Params
duration?: number
- action?: NotificationAction
+ action?: MaybeRef
+ holdTimer?: Ref
+ icon?: Ref
}): void => {
if (params === notificationTypes.danger) {
consola.error('[Notification Error]', message)
@@ -51,25 +58,19 @@ export const showNotification = ({
const componentParams = {
component: h(Notification, {
- title: title,
- message: message!,
- variant: params.variant,
+ title: title ? toRef(title) : title,
+ message: message ? toRef(message!) : message,
+ variant: variant ?? params.variant,
duration: duration,
action: action,
+ holdTimer: holdTimer,
+ icon: icon,
}),
variant: 'component',
duration: 50000, // child component will trigger close when the real duration is ended
}
- Notif.open(
- params.variant === 'success'
- ? {
- message,
- duration: duration,
- closable: true,
- }
- : componentParams,
- )
+ Notif.open(componentParams)
}
export const showLargeNotification = ({
@@ -116,8 +117,8 @@ export const infoMessage = (
export const successMessage = message =>
showNotification({
- title: 'Succes',
- message: `[SUCCESS] ${message}`,
+ title: 'Success',
+ message: message,
params: notificationTypes.success,
})
@@ -149,3 +150,65 @@ export const dangerMessage = (
params: notificationTypes.danger,
action: reportable ? getReportIssueAction(message) : undefined,
})
+
+const ifIsRef = (value: MaybeRef, otherwise: T): T =>
+ Boolean(value) && isRef(value) && unref(value)
+ ? (value.value as T)
+ : otherwise
+
+const NotificationStateToVariantMap: Record<
+ LoadingNotificationState,
+ NeoMessageVariant
+> = {
+ succeeded: 'success',
+ loading: 'neutral',
+ failed: 'danger',
+}
+
+export type LoadingNotificationState = 'loading' | 'succeeded' | 'failed'
+
+export const loadingMessage = ({
+ title,
+ message,
+ state,
+ action,
+}: {
+ title: MaybeRef
+ message?: MaybeRef
+ state: Ref
+ action?: Ref
+}) => {
+ const { $i18n } = useNuxtApp()
+ const stateMessage = ref(unref(message) ?? `${$i18n.t('mint.progress')}...`)
+
+ watch(
+ [state],
+ ([state]) => {
+ if (state === 'succeeded') {
+ stateMessage.value = ifIsRef(
+ message,
+ $i18n.t('transactionLoader.completed'),
+ )
+ }
+ else if (state === 'failed') {
+ stateMessage.value = ifIsRef(message, '')
+ }
+ },
+ {
+ once: true,
+ },
+ )
+
+ const isLoadingState = computed(() => state.value === 'loading')
+
+ showNotification({
+ title,
+ message: stateMessage,
+ variant: computed(() => NotificationStateToVariantMap[state.value]),
+ action: action,
+ holdTimer: isLoadingState,
+ icon: computed(() =>
+ isLoadingState.value ? { icon: 'spinner-third', spin: true } : undefined,
+ ),
+ })
+}