diff --git a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue index db9d97eaa5dd455d970b091deb1cc9ec46f9aaaf..2f5c780cc59d40a619d0705ae50046f03d7c777c 100644 --- a/src/components/onboarding/wallets/metamask/MetamaskConnect.vue +++ b/src/components/onboarding/wallets/metamask/MetamaskConnect.vue @@ -142,9 +142,6 @@ onMounted(() => { void connect(false); }); -const copyContent = (content: string) => { - navigator.clipboard.writeText(String(content)); -}; const generateAccountUpdateTransaction = async(): Promise<string> => { const wax = await getWax(); const tx = await wax.createTransaction(); @@ -251,12 +248,12 @@ const updateAccountName = (value: string | any) => { </div> </div> <div class="flex items-center flex-col"> - <Button :disabled="isLoading" @click="copyContent(getAuthorityUpdateSigningLink())" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> + <Button :disabled="isLoading" :copy="getAuthorityUpdateSigningLink" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> <span class="text-md font-bold">Copy signing link</span> </Button> <Separator label="Or" class="mt-8" /> <div class="flex justify-center mt-4"> - <Button :disabled="isLoading" @click="generateAccountUpdateTransaction()" variant="outline" size="lg" class="px-8 opacity-[0.9] py-4 border-[#FF5C16] border-[1px]"> + <Button :disabled="isLoading" :copy="generateAccountUpdateTransaction" variant="outline" size="lg" class="px-8 opacity-[0.9] py-4 border-[#FF5C16] border-[1px]"> <span class="text-md font-bold">Copy entire transaction</span> </Button> </div> @@ -277,7 +274,7 @@ const updateAccountName = (value: string | any) => { </div> </div> <div class="flex items-center flex-col"> - <Button :disabled="isLoading" @click="copyContent(getAccountCreateSigningLink())" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> + <Button :copy="getAccountCreateSigningLink" :disabled="isLoading" variant="outline" size="lg" class="mt-4 px-8 py-4 border-[#FF5C16] border-[1px]"> <span class="text-md font-bold">Copy signing link</span> </Button> </div> diff --git a/src/components/ui/button/Button.vue b/src/components/ui/button/Button.vue index 3ba549bf4422162af85c42cf4750617820cea54d..ba01e17dceb546df0d75194e598e4e3f5fc8db23 100644 --- a/src/components/ui/button/Button.vue +++ b/src/components/ui/button/Button.vue @@ -1,9 +1,10 @@ <script setup lang="ts"> -import type { HTMLAttributes } from 'vue' +import { ref, type HTMLAttributes } from 'vue' import { cn } from '@/lib/utils' import { Primitive, type PrimitiveProps } from 'reka-ui' import { type ButtonVariants, buttonVariants } from '.' -import { mdiLoading } from '@mdi/js' +import { mdiCheck, mdiLoading } from '@mdi/js' +import { copyText } from '@/utils/copy' interface Props extends PrimitiveProps { variant?: ButtonVariants['variant'] @@ -11,12 +12,31 @@ interface Props extends PrimitiveProps { class?: HTMLAttributes['class'] loading?: boolean disabled?: boolean + copy?: string | (() => (string | Promise<string>)) } const props = withDefaults(defineProps<Props>(), { as: 'button', loading: false }) + +const copyLoading = ref(false); + +const copyBtn = () => { + if (!props.copy) return; + const text = typeof props.copy === 'function' ? props.copy() : props.copy; + if(text instanceof Promise) + text.then((text) => { + copyText(text); + }); + else + copyText(text); + + copyLoading.value = true; + setTimeout(() => { + copyLoading.value = false; + }, 1000); +}; </script> <template> @@ -25,13 +45,19 @@ const props = withDefaults(defineProps<Props>(), { :as-child="asChild" :disabled="loading || disabled" :class="[ cn(buttonVariants({ variant, size }), props.class)]" + @click="copyBtn" > + <span v-if="copy && copyLoading" class="absolute mx-auto"> + <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path :style="{ + fill: !variant || variant === 'default' ? 'hsl(var(--background))' : 'hsl(var(--foreground))' + }" :d="mdiCheck"/></svg> + </span> <span v-if="loading" class="animate-spin absolute mx-auto"> <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path :style="{ fill: !variant || variant === 'default' ? 'hsl(var(--background))' : 'hsl(var(--foreground))' }" :d="mdiLoading"/></svg> </span> - <span :style="{ 'visibility': loading ? 'hidden' : 'visible' }" class="inline-flex items-center justify-center gap-2"> + <span :style="{ 'visibility': loading || copyLoading ? 'hidden' : 'visible' }" class="inline-flex items-center justify-center gap-2"> <slot/> </span> </Primitive> diff --git a/src/components/ui/copybutton/Button.vue b/src/components/ui/copybutton/Button.vue index 6694833d4dfc5257aae85a0608e77e29f9710e74..4aa3c9306779837c5beebeb9b1f66f7bc164c6a6 100644 --- a/src/components/ui/copybutton/Button.vue +++ b/src/components/ui/copybutton/Button.vue @@ -4,6 +4,7 @@ import { cn } from '@/lib/utils' import { Primitive, type PrimitiveProps } from 'reka-ui' import { buttonVariants } from '.' import { mdiCheck, mdiContentCopy } from '@mdi/js' +import { copyText } from '@/utils/copy' interface Props extends PrimitiveProps { class?: HTMLAttributes['class']; @@ -18,7 +19,7 @@ const copyBtn = (event: MouseEvent) => { const target = event.target as HTMLElement; const value = target.getAttribute("data-copy"); if (!value) return; - navigator.clipboard.writeText(String(value)); + copyText(value); const oldAttribute = target.children[0].children[0].getAttribute('d'); target.children[0].children[0].setAttribute('d', mdiCheck); diff --git a/src/utils/copy.ts b/src/utils/copy.ts new file mode 100644 index 0000000000000000000000000000000000000000..b58173c0e3349ac642e47bcc602ea0e1294be8da --- /dev/null +++ b/src/utils/copy.ts @@ -0,0 +1,34 @@ +/** + * Copies given text into clipboard. + * + * This function supports 3 ways of copying and fallbacks if any of them fails: + * 1. Using the modern Clipboard API (navigator.clipboard.writeText) - on most browsers works only in secure context (HTTPS) or localhost + * 2. Using the deprecated document.execCommand("copy") - deprecated, but still supported by most browsers - with Firefox works only on "click" event triggered by the user + * 3. Using the prompt function - the most universal way, but requires user interaction + */ +export const copyText = (text: string) => { + try { + if (navigator.clipboard) { // is secure context + return navigator.clipboard.writeText(text).catch(() => { + prompt("Copy to clipboard: Ctrl+C, Enter", text); + }); + } else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { // check if we can use deprecated copy command + const textarea = document.createElement("textarea"); + textarea.textContent = text; + textarea.style.width = "0"; + textarea.style.height = "0"; + textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in MS Edge. + document.body.appendChild(textarea); + textarea.select(); + try { + if(!document.execCommand("copy")) // Security exception may be thrown by some browsers. + throw new Error("Copy command was unsuccessful"); + } finally { + document.body.removeChild(textarea); + } + } + throw new Error("No clipboard support"); + } catch { + prompt("Copy to clipboard: Ctrl+C, Enter", text); + } +}