diff options
| author | Adam Soltys <[email protected]> | 2024-09-10 20:39:47 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2024-09-17 09:18:36 -0700 |
| commit | 1f63848ca9fb01336f04b41434de1cc06a5b3d93 (patch) | |
| tree | 2affa3041863be365f33d99e1f32bda60cf93a32 /src/routes/(app)/settings | |
| parent | 6e48c584605bec5623c12c470d04d07303983801 (diff) | |
| download | coinos-ui-1f63848ca9fb01336f04b41434de1cc06a5b3d93.tar.gz coinos-ui-1f63848ca9fb01336f04b41434de1cc06a5b3d93.zip | |
move routes
Diffstat (limited to 'src/routes/(app)/settings')
| -rw-r--r-- | src/routes/(app)/settings/[...tab]/+page.server.ts | 34 | ||||
| -rw-r--r-- | src/routes/(app)/settings/[...tab]/+page.svelte | 257 | ||||
| -rw-r--r-- | src/routes/(app)/settings/[...tab]/_account.svelte | 190 | ||||
| -rw-r--r-- | src/routes/(app)/settings/[...tab]/_pos.svelte | 238 | ||||
| -rw-r--r-- | src/routes/(app)/settings/[...tab]/_security.svelte | 321 | ||||
| -rw-r--r-- | src/routes/(app)/settings/[...tab]/_shopify.svelte | 92 |
6 files changed, 1132 insertions, 0 deletions
diff --git a/src/routes/(app)/settings/[...tab]/+page.server.ts b/src/routes/(app)/settings/[...tab]/+page.server.ts new file mode 100644 index 00000000..42639b38 --- /dev/null +++ b/src/routes/(app)/settings/[...tab]/+page.server.ts @@ -0,0 +1,34 @@ +import { fail, redirect } from "@sveltejs/kit"; +import { fd, auth, get, post } from "$lib/utils"; + +export function load({ cookies, params, url }) { + if (url.pathname.endsWith("settings")) + redirect(307, url.pathname + "/account"); + params.cookies = cookies.getAll(); + return { cookies: cookies.getAll(), tab: params.tab }; +} + +export const actions = { + default: async ({ cookies, request }) => { + let form = await fd(request); + + if (form.tab === "pos") { + form.notify = form.notify === "on"; + form.nip5 = form.nip5 === "on"; + form.prompt = form.prompt === "on"; + form.autowithdraw = form.autowithdraw === "on"; + } + + let user = { ...(await get("/me", auth(cookies))), ...form }; + + try { + ({ user } = await post(`/user`, user, auth(cookies))); + } catch (e: any) { + return fail(400, { message: e.message }); + } + + if (user.language) cookies.set("lang", user.language, { path: "/" }); + + return { user, success: true }; + }, +}; diff --git a/src/routes/(app)/settings/[...tab]/+page.svelte b/src/routes/(app)/settings/[...tab]/+page.svelte new file mode 100644 index 00000000..c495a5cc --- /dev/null +++ b/src/routes/(app)/settings/[...tab]/+page.svelte @@ -0,0 +1,257 @@ +<script> + import { browser } from "$app/environment"; + import { onMount, tick } from "svelte"; + import { fly } from "svelte/transition"; + import { applyAction, deserialize } from "$app/forms"; + + import Icon from "$comp/Icon.svelte"; + import Spinner from "$comp/Spinner.svelte"; + import Pin from "$comp/Pin.svelte"; + import { loading, t } from "$lib/translations"; + import { fail, auth, post, sleep, warning, success } from "$lib/utils"; + import { avatar, banner, password, pin } from "$lib/store"; + import { upload } from "$lib/upload"; + import { page } from "$app/stores"; + import { sign, send, reEncryptEntropy, setNsec } from "$lib/nostr"; + import { invalidateAll } from "$app/navigation"; + + import Account from "./_account.svelte"; + import Pos from "./_pos.svelte"; + import Security from "./_security.svelte"; + import Shopify from "./_shopify.svelte"; + + import { PUBLIC_COINOS_URL } from "$env/static/public"; + + export let data; + export let form; + + let submit; + let formElement; + + let { user, token, rates, tab, cookies } = data; + let prev; + + $: update(data); + let update = () => { + ({ user, token, rates, tab } = data); + prev = { ...user }; + }; + + $: form?.user && ({ user } = form); + + $: form?.success && throttledSuccess(); + + let justUpdated; + let throttledSuccess = () => { + if (!justUpdated) { + justUpdated = true; + success($t("user.settings.saved"), false); + setTimeout(() => (justUpdated = false), 5000); + } + }; + + $: form?.message && fail(form.message); + + $: if (form?.message?.startsWith("Pin")) { + fail("Wrong pin, try again"); + $pin = ""; + } + + let tabs = [ + { name: "account", key: "ACCOUNT", comp: Account }, + { name: "pos", key: "POINT_OF_SALE", comp: Pos }, + { name: "security", key: "SECURITY", comp: Security }, + { name: "shopify", key: "SHOPIFY", comp: Shopify }, + ]; + + $: ({ comp } = tabs.find((t) => t.name === tab)); + + let { address, id, username } = user; + let submitting; + + $: if (!$loading && $page.url.searchParams.get("verified")) + success($t("user.settings.verified")); + + async function handleSubmit() { + try { + submitting = true; + let data = new FormData(formElement); + + if (data.get("newNsec")) { + await setNsec(user, data.get("newNsec")); + data.set("nsec", user.nsec); + data.set("pubkey", user.pubkey); + } + + if (data.get("password")) { + try { + data.set( + "cipher", + await reEncryptEntropy(user, data.get("password")) + ); + } catch (e) { + console.log("Failed to encrypt keys with new password"); + throw e; + } + } + + if ($avatar) { + try { + let { hash } = JSON.parse( + await upload($avatar.file, $avatar.type, $avatar.progress, token) + ); + + data.set("profile", hash); + + await fetch(`/api/public/${hash}.webp`, { + cache: "reload", + mode: "no-cors", + }); + } catch (e) { + console.log("problem upsubmitting avatar", e); + } + } + + if ($banner) { + try { + let { hash } = JSON.parse( + await upload($banner.file, $banner.type, $banner.progress, token) + ); + + data.set("banner", hash); + + await fetch(`/api/public/${hash}.webp`, { + cache: "reload", + mode: "no-cors", + }); + } catch (e) { + console.log("problem upsubmitting banner", e); + } + } + + if (["username", "address", "profile"].some((a) => user[a] !== prev[a])) { + let event = { + pubkey: user.pubkey, + created_at: Math.floor(Date.now() / 1000), + kind: 0, + content: JSON.stringify({ + name: user.username, + about: user.address, + picture: `${PUBLIC_COINOS_URL}/public/${user.profile}.webp`, + }), + tags: [], + }; + + try { + await sign({ event, user }); + await send(event); + } catch (e) { + warning("Nostr profile could not be updated"); + } + } + + if (user.email !== prev.email) { + try { + cookies.get = function (n) { + return this.find((c) => c.name === n).value; + }; + + user.verified = false; + + await post( + "/api/request", + { id: user.id, email: user.email }, + auth(cookies) + ); + + warning($t("user.settings.verifying"), false); + } catch (e) { + fail(e.message); + console.log(e); + } + } + + const response = await fetch(formElement.action, { + method: "POST", + body: data, + }); + + const result = deserialize(await response.text()); + + if (result.type === "success") { + await invalidateAll(); + if (data.get("password")) $password = data.get("password"); + } + + applyAction(result); + } catch (e) { + console.log(e); + fail("Something went wrong"); + } + + submitting = false; + } +</script> + +{#if user.haspin && $pin?.length !== 6} + <Pin /> +{/if} + +<form + method="POST" + class="mb-[154px] settings" + on:submit|preventDefault={handleSubmit} + bind:this={formElement} +> + <input type="hidden" name="pin" value={$pin} /> + <input type="hidden" name="tab" value={tab} /> + + <div class="mt-24 mb-20 px-3 md:px-0 w-full md:w-[400px] mx-auto space-y-8"> + <div class="header"> + <h1 class="text-center text-3xl md:text-4xl font-semibold mb-10"> + {$t("user.settings.header")} + </h1> + + <div + class="font-bold flex justify-around items-center border-b pb-3 text-secondary" + > + {#each tabs.filter((t) => t.name !== "shopify") as { name, key }} + <a href={`/settings/${name}`}> + <button + type="button" + class="hover:opacity-80" + class:text-black={tab === name || + (tab === "shopify" && name === "pos")} + >{$t(`user.settings.${key}`)}</button + > + </a> + {/each} + </div> + </div> + + <svelte:component this={comp} {user} {rates} {submit} /> + </div> + + <div + class="z-10 fixed bottom-0 bg-white shadow border-t w-full flex justify-center items-center py-3" + > + <button + bind:this={submit} + type="submit" + class="border-2 border-black rounded-xl font-semibold mx-auto py-3 w-40 hover:opacity-80" + class:bg-black={submitting} + > + {#if submitting} + <Spinner /> + {:else} + <div class="my-auto">{$t("user.settings.saveSettings")}</div> + {/if} + </button> + </div> +</form> + +<style> + .settings { + view-transition-name: settings; + } +</style> diff --git a/src/routes/(app)/settings/[...tab]/_account.svelte b/src/routes/(app)/settings/[...tab]/_account.svelte new file mode 100644 index 00000000..8e3323c7 --- /dev/null +++ b/src/routes/(app)/settings/[...tab]/_account.svelte @@ -0,0 +1,190 @@ +<script> + import { tick } from "svelte"; + import { avatar, banner } from "$lib/store"; + import Icon from "$comp/Icon.svelte"; + import { t } from "$lib/translations"; + import { page } from "$app/stores"; + + export let user, rates, submit; + + let { id } = user; + + let avatarFile, avatarInput, bannerFile, bannerInput; + + let selectAvatar = () => avatarInput.click(); + let selectBanner = () => bannerInput.click(); + + let percent; + let progress = async (event) => { + percent = Math.round((event.loaded / event.total) * 100); + }; + + let tooLarge = {}; + + let handleFile = async ({ target }, type) => { + tooLarge[type] = false; + let file = target.files[0]; + if (!file) return; + + if (file.size > 10000000) return (tooLarge[type] = true); + + if (type === "profile") { + $avatar = { id, file, type, progress }; + } else if (type === "banner") { + $banner = { id, file, type, progress }; + } + + var reader = new FileReader(); + reader.onload = async (e) => { + if (type === "profile") { + $avatar.src = e.target.result; + } else if (type === "banner") { + $banner.src = e.target.result; + } + }; + + reader.readAsDataURL(file); + }; + + if (!user.display) user.display = user.username; + + $: url = `${$page.url.host}/${user.username}`; + $: full = `${$page.url.protocol}//${url}`; + $: addr = `${user.username}@${$page.url.host}`; +</script> + +<div> + <label for="username" class="font-bold mb-1 block" + >{$t("user.settings.username")}</label + > + <div class="flex mb-2"> + <input + type="text" + name="username" + bind:value={user.username} + class="w-auto min-w-0 pr-1 grow" + /> + </div> +</div> + +<div> + <label for="display" class="font-bold mb-1 block" + >{$t("user.settings.displayName")}</label + > + <input type="text" name="display" bind:value={user.display} /> +</div> + +<div class="space-y-1 relative"> + <label for="email" class="font-bold block">{$t("user.settings.email")}</label> + <p class="text-secondary"> + {$t("user.settings.emailDescription")} + </p> + + <div class="relative"> + <input type="text" name="email" bind:value={user.email} /> + {#if user.verified} + <Icon icon="check" style="absolute right-5 top-3 w-8" /> + {:else if user.email} + <Icon icon="orangeclock" style="absolute right-5 top-3 w-8" /> + {/if} + </div> +</div> + +<div> + <span class="font-bold">{$t("user.settings.profileImage")}</span> + + <div class="flex"> + {#if $avatar || user.profile} + <div + class="relative rounded-full overflow-hidden text-center w-20 h-20 my-auto hover:opacity-80 cursor-pointer" + on:click={selectAvatar} + on:keydown={selectAvatar} + > + <img + src={$avatar?.src || `/api/public/${user.profile}.webp`} + class="absolute w-full h-full object-cover object-center visible overflow-hidden" + alt={user.username} + /> + </div> + {:else} + <div + class="rounded-full border-4 border-white p-4 bg-gradient-to-r from-primary to-gradient w-24 my-auto hover:opacity-80 cursor-pointer" + on:click={selectAvatar} + on:keydown={selectAvatar} + > + <Icon icon="logo-symbol-white" style="mx-auto" /> + </div> + {/if} + <div class="ml-2 p-2"> + <!-- found missing translation --> + <button + type="button" + class="border rounded-2xl font-bold w-24 text-center px-0 py-2 hover:opacity-80" + on:click={selectAvatar} + on:keydown={selectAvatar}>{$t("user.settings.select")}</button + > + <input + type="file" + class="hidden" + bind:this={avatarInput} + on:change={(e) => handleFile(e, "profile")} + /> + </div> + </div> + + {#if tooLarge["avatar"]} + <div class="text-red-600">Max file size 10MB</div> + {/if} +</div> + +<div> + <div class="flex justify-between items-center"> + <span class="font-bold">{$t("user.settings.bannerImage")}</span> + </div> + + {#if $banner || user.banner} + <img + src={$banner ? $banner.src : `/api/public/${user.banner}.webp`} + class="w-full object-cover object-center visible overflow-hidden h-48 mb-4 hover:opacity-80" + on:click={selectBanner} + on:keydown={selectBanner} + alt="Banner" + /> + {:else} + <div + class="bg-gradient-to-r from-primary to-gradient w-full h-48 mb-4 cursor-pointer hover:opacity-80" + on:click={selectBanner} + on:keydown={selectBanner} + alt="Banner" + /> + {/if} + + <button + type="button" + class="border rounded-2xl font-bold w-24 text-center px-0 py-2 hover:opacity-80" + on:click={selectBanner} + on:keydown={selectBanner}>{$t("user.settings.select")}</button + > + <input + type="file" + class="hidden" + bind:this={bannerInput} + on:change={(e) => handleFile(e, "banner")} + /> + + {#if tooLarge["banner"]} + <div class="text-red-600">Max file size 10MB</div> + {/if} +</div> + +<div> + <label for="address" class="font-bold mb-1 block" + >{$t("user.settings.about")}</label + > + <textarea + type="text" + name="address" + bind:value={user.address} + placeholder={$t("user.settings.aboutPlaceholder")} + /> +</div> diff --git a/src/routes/(app)/settings/[...tab]/_pos.svelte b/src/routes/(app)/settings/[...tab]/_pos.svelte new file mode 100644 index 00000000..6a1c92a1 --- /dev/null +++ b/src/routes/(app)/settings/[...tab]/_pos.svelte @@ -0,0 +1,238 @@ +<script> + import { tick } from "svelte"; + import Icon from "$comp/Icon.svelte"; + import Numpad from "$comp/Numpad.svelte"; + import LocaleSelector from "$comp/LocaleSelector.svelte"; + import Toggle from "$comp/Toggle.svelte"; + import { locale, t } from "$lib/translations"; + import { success, fail } from "$lib/utils"; + import { page } from "$app/stores"; + + export let user, rates, submit; + + let { currency } = user; + let rate = rates[currency]; + + let fiats = Object.keys(rates).sort((a,b) => a.localeCompare(b)); + $: user.language = $locale; + let keypress = (e) => e.key === "Enter" && (e.preventDefault() || el.click()); + + let editingReserve, editingThreshold, doneReserve, doneThreshold; + let doneEditing = () => { + editingReserve = false; + editingThreshold = false; + }; + + let editReserve = async () => { + editingReserve = true; + await tick(); + reserveEl.focus(); + }; + + let editThreshold = async () => { + editingThreshold = true; + await tick(); + thresholdEl.focus(); + }; + + if (!user.threshold) user.threshold = 1000000; + if (!user.reserve) user.reserve = 100000; + let reserveEl, thresholdEl; +</script> + +<div> + <label for="language" class="font-bold block mb-1" + >{$t("user.settings.locale")}</label + > + <LocaleSelector style="select-styles block py-3 w-full" /> +</div> + +<div> + <label for="currency" class="font-bold block mb-1" + >{$t("user.settings.localCurrency")}</label + > + <select + name="currency" + class="select-styles block py-3 w-full" + value={user.currency} + > + {#each fiats as fiat} + <option value={fiat}>{fiat}</option> + {/each} + </select> +</div> + +<div> + <label for="memoPrompt" class="font-bold mb-1 block" + >{$t("user.settings.memoPrompt")}</label + > + <textarea + name="memoPrompt" + placeholder={$t("user.settings.memoPromptPlaceholder")} + bind:value={user.memoPrompt} + /> +</div> + +<div> + <div class="flex justify-between items-center"> + <span class="font-bold">{$t("user.settings.notifications")}</span> + <Toggle id="notify" bind:value={user.notify} /> + </div> + <p class="text-secondary mt-1 w-9/12"> + {$t("user.settings.notificationsDesc")} + </p> +</div> + +<div> + <div class="flex justify-between items-center"> + <span class="font-bold">{$t("user.settings.tipPrompt")}</span> + <Toggle id="prompt" bind:value={user.prompt} /> + </div> + <p class="text-secondary mt-1 w-9/12"> + {$t("user.settings.tipPromptDescription")} + </p> +</div> + +<div> + <div class="flex justify-between items-center"> + <span class="font-bold">{$t("user.settings.publishNip5")}</span> + <Toggle id="nip5" bind:value={user.nip5} /> + </div> + <p class="text-secondary mt-1 w-9/12"> + {$t("user.settings.nip5a")} + <span class="font-bold">{user.username}@{$page.url.host}</span> + {$t("user.settings.nip5b")} + </p> +</div> + +<div> + <div class="flex justify-between items-center"> + <span class="font-bold">{$t("user.settings.autoWithdraw")}</span> + <Toggle id="autowithdraw" bind:value={user.autowithdraw} /> + </div> + <p class="text-secondary mt-1 w-9/12"> + {$t("user.settings.autoWithdrawDescription")} + </p> +</div> + +{#if user.autowithdraw} + <div class="mb-2"> + <label for="display" class="font-bold mb-1 block" + >{$t("user.settings.destination")}</label + > + <textarea + name="destination" + placeholder={$t("user.settings.destinationPlaceholder")} + on:keypress={keypress} + class="w-full p-4 border rounded-xl h-48" + bind:value={user.destination} + /> + </div> + + <div> + <label for="display" class="font-bold mb-1 block" + >{$t("user.settings.threshold")}</label + > + <button type="button" class="flex w-full" on:click={editThreshold}> + <div class="p-4 border rounded-2xl rounded-r-none border-r-0 bg-primary"> + ⚡️ + </div> + <div + class="border-l-0 rounded-l-none pl-2 w-full p-4 border rounded-2xl text-left" + > + {user.threshold} + </div> + <input type="hidden" name="threshold" bind:value={user.threshold} /> + </button> + <p class="text-secondary mt-1"> + {$t("user.settings.thresholdDesc")} + </p> + </div> + + <div> + <label for="display" class="font-bold mb-1 block" + >{$t("user.settings.reserve")}</label + > + <button type="button" class="flex w-full" on:click={editReserve}> + <div class="p-4 border rounded-2xl rounded-r-none border-r-0 bg-primary"> + ⚡️ + </div> + <div + class="border-l-0 rounded-l-none pl-2 w-full p-4 border rounded-2xl text-left" + > + {user.reserve} + </div> + <input type="hidden" name="reserve" bind:value={user.reserve} /> + </button> + <p class="text-secondary mt-1"> + {$t("user.settings.reserveDesc")} + </p> + </div> +{/if} + +{#if editingThreshold} + <div + class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50" + > + <div + class="relative top-40 mx-auto p-5 border shadow-lg rounded-md bg-white space-y-5 w-lg max-w-lg" + > + <h1 class="text-center text-2xl font-semibold"> + {$t("user.settings.threshold")} + </h1> + <Numpad + bind:amount={user.threshold} + bind:currency + bind:rate + bind:submit={doneReserve} + bind:element={thresholdEl} + /> + + <button + bind:this={doneReserve} + type="button" + on:click={doneEditing} + class="p-4 bg-black text-white rounded-2xl text-lg font-semibold w-full" + >Ok</button + > + </div> + </div> +{/if} + +{#if editingReserve} + <div + class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50" + > + <div + class="relative top-40 mx-auto p-5 border shadow-lg rounded-md bg-white space-y-5 w-lg max-w-lg text-center" + > + <h1 class="text-2xl font-semibold"> + {$t("user.settings.reserve")} + </h1> + <Numpad + bind:amount={user.reserve} + bind:currency + bind:rate + bind:submit={doneReserve} + bind:element={reserveEl} + /> + <button + bind:this={doneReserve} + type="button" + on:click={doneEditing} + class="p-4 bg-black text-white rounded-2xl text-lg font-semibold w-full" + >Ok</button + > + </div> + </div> +{/if} + +<a href="./shopify"> + <button + type="button" + class="flex rounded-full border py-2 px-4 font-bold hover:opacity-80 w-full gap-2 my-5 justify-center" + > + <img src="/icons/shopify.svg" class="w-8" /> + <div class="my-auto">{$t("user.settings.shopify")}</div> + </button> +</a> diff --git a/src/routes/(app)/settings/[...tab]/_security.svelte b/src/routes/(app)/settings/[...tab]/_security.svelte new file mode 100644 index 00000000..6b44f830 --- /dev/null +++ b/src/routes/(app)/settings/[...tab]/_security.svelte @@ -0,0 +1,321 @@ +<script> + import { getMnemonic, getNsec } from "$lib/nostr"; + import { tick } from "svelte"; + import { t } from "$lib/translations"; + import Pin from "$comp/Pin.svelte"; + import Icon from "$comp/Icon.svelte"; + import Qr from "$comp/Qr.svelte"; + import { copy, post, success, fail } from "$lib/utils"; + import { pin as current } from "$lib/store"; + import { invalidate } from "$app/navigation"; + import { page } from "$app/stores"; + + export let user, rates, submit; + + let confirming2fa, + disabling2fa, + importing, + locked, + mnemonic, + newNsec, + nsec, + old, + otp, + password, + pin, + revealPassword, + revealSeed, + revealNsec, + token = "", + setting2fa, + settingPin, + verify; + + let toggleImporting = () => { + revealNsec = false; + importing = !importing; + }; + + let toggleNsec = async () => { + try { + nsec = await getNsec(user); + revealNsec = !revealNsec; + importing = false; + } catch (e) { + console.log(e); + } + }; + + let togglePassword = () => (revealPassword = !revealPassword); + let toggleSeed = async () => { + try { + mnemonic = await getMnemonic(user); + revealSeed = !revealSeed; + } catch (e) { + console.log(e); + } + }; + + $: verifying = pin?.length > 5; + $: verify && checkPin(pin); + let checkPin = async () => { + old = $current; + + try { + if (pin.length > 5 && pin === verify) { + user.haspin = true; + $current = pin; + pin = ""; + verify = ""; + submit.click(); + settingPin = false; + verifying = false; + } else { + fail("Pin mismatch, try again"); + pin = ""; + verify = ""; + verifying = false; + settingPin = true; + } + } catch (e) { + console.log(e); + fail("Problem setting PIN"); + } + }; + + let reset = () => { + token = ""; + return true; + }; + + let disablingPin = false; + + let togglePin = async () => { + if (user.haspin) { + try { + disablingPin = true; + await tick(); + submit.click(); + } catch (e) { + console.log(e); + fail("Failed to disable pin"); + } + } else { + settingPin = true; + disablingPin = false; + } + }; + + let startEnabling2fa = async () => { + reset(); + try { + if (!otp) otp = await post("/otpsecret", { pin: $current }); + } catch (e) { + console.log(e); + } + + setting2fa = true; + }; + + let startDisabling2fa = () => (disabling2fa = true); + let startConfirming2fa = () => (confirming2fa = true); + let cancel = () => { + pin = null; + token = null; + verifying = false; + settingPin = false; + setting2fa = false; + confirming2fa = false; + disabling2fa = false; + }; + + $: enable2fa(token); + let enable2fa = async (twoFa) => { + try { + if (setting2fa && token.length === 6) { + await post("/enable2fa", { token }); + success("2FA enabled"); + user.twofa = 1; + cancel(); + } + } catch (e) { + fail("Failed to enable 2FA, try again"); + } + }; + + $: disable2fa(token); + let disable2fa = async () => { + try { + if (disabling2fa && token.length === 6) { + await post("/disable2fa", { token }); + success("2FA disabled"); + delete user.twofa; + disabling2fa = false; + } + } catch (e) { + console.log(e); + fail("Failed to disable 2FA, try again"); + } + }; +</script> + +<input type="hidden" name="newpin" value={disablingPin ? "delete" : pin} /> +<input type="hidden" name="confirm" bind:value={password} /> + +<div class="relative mb-5"> + <label for="password" class="block font-bold block mb-1" + >{$t("user.settings.newPassword")}</label + > + {#if revealPassword} + <input name="password" type="text" bind:value={password} /> + {:else} + <input name="password" type="password" bind:value={password} /> + {/if} + <button + type="button" + on:click={togglePassword} + on:keydown={togglePassword} + class="absolute right-5 top-11" + > + <Icon icon={revealPassword ? "eye" : "eye-off"} /> + </button> +</div> + +<div> + <span class="font-bold mb-1" + >{verifying + ? $t("user.settings.verifyPIN") + : $t("user.settings.securityPIN")}</span + > + <p class="text-secondary mb-1"> + {$t("user.settings.securityPINDescription")} + </p> + {#if verifying} + <Pin bind:value={verify} {cancel} notify={false} /> + {:else if settingPin} + <Pin + bind:value={pin} + title={$t("user.settings.setPIN")} + {cancel} + notify={false} + /> + {:else} + <button type="button" class="primary" on:click={togglePin} + ><Icon icon="lock" style="mr-1" /> + {user.haspin + ? $t("user.settings.disablePIN") + : $t("user.settings.enablePIN")}</button + > + {/if} +</div> + +<div> + <span class="font-bold mb-1">{$t("user.settings.twofa")}</span> + <p class="text-secondary mb-4"> + {$t("user.settings.twofaDescription")} + </p> + + {#if setting2fa} + <a href={otp.uri}> + <Qr src={otp.qr} /> + </a> + + <div class="text-center my-4"> + {$t("user.settings.accountId")}<br /> + <b>{otp.secret}</b> + </div> + + <button type="button" class="primary" on:click={startConfirming2fa}> + <Icon icon="numpad" style="w-8 mr-1" /> + <div class="my-auto">Confirm</div> + </button> + {:else if user.twofa} + <button type="button" class="primary" on:click={startDisabling2fa}> + <Icon icon="mobile" style="w-8 mr-1" /> + <div class="my-auto">{$t("user.settings.twofaDisable")}</div> + </button> + {:else} + <button type="button" class="primary" on:click={startEnabling2fa}> + <Icon icon="mobile" style="w-8 mr-1" /> + <div class="my-auto">{$t("user.settings.twofaSetup")}</div> + </button> + {/if} + + {#if confirming2fa || disabling2fa} + <Pin + bind:value={token} + title="Enter 2FA Code" + {cancel} + persist={false} + notify={false} + /> + {/if} +</div> + +<!-- <div> --> +<!-- <label for="seedphrase" class="font-bold" --> +<!-- >{$t("user.settings.seedPhrase")}</label --> +<!-- > --> +<!-- --> +<!-- <p class="text-secondary mb-1"> --> +<!-- {$t("user.settings.seedDescription")} --> +<!-- </p> --> +<!-- --> +<!-- <button type="button" class="primary" on:click={toggleSeed}> --> +<!-- <Icon icon="warning" style="mr-1 w-6 my-auto" /> --> +<!-- {revealSeed ? $t("user.settings.hideSeed") : $t("user.settings.revealSeed")} --> +<!-- </button> --> +<!-- --> +<!-- {#if revealSeed} --> +<!-- <div class="text-lg"> --> +<!-- {mnemonic} --> +<!-- </div> --> +<!-- --> +<!-- <button type="button" class="primary" on:click={() => copy(mnemonic)}> --> +<!-- <Icon icon="copy" style="mr-1 w-6 my-auto" /> --> +<!-- Copy --> +<!-- </button> --> +<!-- {/if} --> +<!-- </div> --> + +<div> + <label for="seedphrase" class="font-bold" + >{$t("user.settings.nostrKeys")}</label + > + + <p class="text-secondary mb-1"> + {$t("user.settings.nostrDescription")} + </p> + + <div class="flex"> + <button type="button" class="primary" on:click={toggleNsec}> + {#if revealNsec} + <Icon icon="eye-off" style="mr-1 w-6 my-auto" /> + {$t("user.settings.hideNsec")} + {:else} + <Icon icon="warning" style="mr-1 w-6 my-auto" /> + {$t("user.settings.revealNsec")} + {/if} + </button> + + <button type="button" class="primary" on:click={toggleImporting}> + <Icon icon="save" style="mr-1 w-6 my-auto" /> + {$t("user.settings.import")} + </button> + </div> + + {#if importing} + <input name="newNsec" bind:value={newNsec} placeholder="nsec..." /> + {/if} + + {#if revealNsec} + <div class="text-lg break-all"> + {nsec} + </div> + + <button type="button" class="primary" on:click={() => copy(nsec)}> + <Icon icon="copy" style="mr-1 w-6 my-auto" /> + Copy + </button> + {/if} +</div> diff --git a/src/routes/(app)/settings/[...tab]/_shopify.svelte b/src/routes/(app)/settings/[...tab]/_shopify.svelte new file mode 100644 index 00000000..0266fa9c --- /dev/null +++ b/src/routes/(app)/settings/[...tab]/_shopify.svelte @@ -0,0 +1,92 @@ +<script> + import { t } from "$lib/translations"; + import Icon from "$comp/Icon.svelte"; + import { page } from "$app/stores"; + + export let user; + $: paymentsUrl = + user.shopifyStore && + "https://admin.shopify.com/store/" + + user.shopifyStore + + "/settings/payments"; + $: checkoutUrl = + user.shopifyStore && + "https://admin.shopify.com/store/" + + user.shopifyStore + + "/settings/checkout"; + $: appsUrl = + user.shopifyStore && + "https://admin.shopify.com/store/" + + user.shopifyStore + + "/settings/apps/development"; + + let script = `<script src="${$page.url.protocol}//${$page.url.host}/${user.username}/shopify.js"><\/script>`; +</script> + +<div> + <label for="shopifyStore" class="font-bold mb-1 block">Store ID</label> + <div class="flex mb-2"> + <div + class="text-gray-600 p-4 my-auto bg-gray-100 border rounded-l-2xl border-r-0 pr-1" + > + admin.shopify.com/store/ + </div> + <input + type="text" + name="shopifyStore" + bind:value={user.shopifyStore} + class="border-l-0 rounded-l-none w-auto min-w-0 pl-1 grow" + /> + </div> + <p class="text-secondary my-2"> + Add Bitcoin under <a + class:active={!!paymentsUrl} + href={paymentsUrl || undefined}>Payments > Manual payment methods</a + > with a description like "Pay with Bitcoin". + </p> +</div> + +<div> + <label for="display" class="font-bold mb-1 block">Additional scripts</label> + <p class="text-secondary my-2"> + Copy this into <a class:active={!!paymentsUrl} href={checkoutUrl} + >Checkout -> Additional scripts</a + > + </p> + <textarea rows={5} type="text" name="shopifyScripts" readonly + >{script}</textarea + > +</div> + +<div> + <label for="shopifyToken" class="font-bold mb-1 block" + >Admin API token <span class="font-normal">(Optional)</span></label + > + <p class="text-secondary my-2"> + Coinos can automatically mark orders as Paid if you provide an API token. + <input + class="my-4" + type="text" + name="shopifyToken" + bind:value={user.shopifyToken} + /> + </p> + <p class="text-secondary my-2"> + Create a new app in <a class:active={!!appsUrl} href={appsUrl || undefined} + >Apps > App Development</a + > + </p> + + <p class="text-secondary my-2"> + Edit the Admin API scopes to enable <code class="text-black" + >write_orders</code + > permission then install the app into your store and copy the Admin API access + token here. + </p> +</div> + +<style> + .active { + @apply text-blue-500; + } +</style> |
