aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/components/AppForm.svelte163
-rw-r--r--src/locales/en.json24
-rw-r--r--src/routes/(app)/apps/[pubkey]/+page.server.ts26
-rw-r--r--src/routes/(app)/apps/[pubkey]/+page.svelte19
-rw-r--r--src/routes/(app)/apps/new/+page.server.ts31
-rw-r--r--src/routes/(app)/apps/new/+page.svelte19
-rw-r--r--src/routes/(app)/qr/[text]/+page.server.ts3
-rw-r--r--src/routes/(app)/settings/nostr/+page.server.ts7
-rw-r--r--src/routes/(app)/settings/nostr/+page.svelte151
-rw-r--r--static/images/alby.svg65
10 files changed, 469 insertions, 39 deletions
diff --git a/src/components/AppForm.svelte b/src/components/AppForm.svelte
new file mode 100644
index 00000000..ffdd3d26
--- /dev/null
+++ b/src/components/AppForm.svelte
@@ -0,0 +1,163 @@
+<script>
+ import Numpad from "$comp/Numpad.svelte";
+ import { getPublicKey } from "nostr-tools";
+ import { onMount, tick } from "svelte";
+ import { bytesToHex, randomBytes } from "@noble/hashes/utils";
+ import { goto } from "$app/navigation";
+ import { copy, focus, fail, post } from "$lib/utils";
+ import { t } from "$lib/translations";
+ import { enhance } from "$app/forms";
+ let { rate, user, name, max_amount, budget_renewal, pubkey, secret } =
+ $props();
+ let { currency } = $derived(user);
+
+ onMount(() => {
+ if (!pubkey) secret || generate();
+ if (!budget_renewal) budget_renewal = "never";
+ });
+
+ let generate = () => {
+ secret = bytesToHex(randomBytes(32));
+ pubkey = getPublicKey(secret);
+ };
+
+ let del = async () => {
+ try {
+ await post(`/post/apps/delete`, { pubkey });
+ goto("/settings/nostr");
+ } catch (e) {
+ fail(e.message);
+ }
+ };
+
+ let settingBudget = $state();
+ let setBudget = () => {
+ settingBudget = false;
+ };
+ let done = $state();
+ let startSetting = async () => {
+ settingBudget = true;
+ await tick();
+ numpad.focus();
+ };
+ let numpad = $state();
+</script>
+
+<form use:enhance method="POST" class="space-y-5">
+ <div>
+ <label for="name" class="font-bold mb-1 block">{$t("accounts.name")}</label>
+ <input type="text" name="name" bind:value={name} use:focus />
+ </div>
+
+ <div>
+ <label for="name" class="font-bold mb-1 block"
+ >{$t("accounts.budget")}</label
+ >
+
+ <button type="button" class="flex w-full" onclick={startSetting}>
+ <div class="p-4 border rounded-2xl rounded-r-none border-r-0 bg-base-200">
+ <iconify-icon
+ noobserver
+ icon="ph:lightning-fill"
+ class="text-yellow-300"
+ ></iconify-icon>
+ </div>
+ <div
+ class="border-l-0 rounded-l-none pl-2 w-full p-4 border rounded-2xl text-left"
+ >
+ {max_amount || $t("accounts.none")}
+ </div>
+ <input type="hidden" name="max_amount" bind:value={max_amount} />
+ </button>
+ </div>
+
+ <div>
+ <label for="name" class="font-bold mb-1 block"
+ >{$t("accounts.renewal")}</label
+ >
+ <select name="budget_renewal" bind:value={budget_renewal}>
+ <option value="never" selected>{$t("accounts.never")}</option>
+ <option value="daily">{$t("accounts.daily")}</option>
+ <option value="weekly">{$t("accounts.weekly")}</option>
+ <option value="monthly">{$t("accounts.monthly")}</option>
+ <option value="yearly">{$t("accounts.yearly")}</option>
+ </select>
+ </div>
+
+ <div>
+ <label for="pubkey" class="font-bold mb-1 block"
+ >{$t("accounts.pubkey")}</label
+ >
+ <div class="flex gap-1 items-center">
+ <textarea
+ rows={3}
+ name="pubkey"
+ bind:value={pubkey}
+ class="grow"
+ readonly={secret}
+ />
+ <button
+ type="button"
+ onclick={() => copy(pubkey)}
+ class="btn !w-auto shrink"
+ >
+ <iconify-icon icon="ph:copy-bold" width="32"></iconify-icon>
+ </button>
+ </div>
+ </div>
+
+ {#if !pubkey || secret}
+ <div>
+ <label for="secret" class="font-bold mb-1 block"
+ >{$t("accounts.secret")}</label
+ >
+ <div class="flex gap-1 items-center">
+ <textarea rows={3} name="secret" bind:value={secret} lcass="grow" />
+
+ <div class="space-y-2 w-24">
+ <button type="button" onclick={generate} class="btn">
+ <iconify-icon noobserver icon="ph:dice-three-bold" width="32"
+ ></iconify-icon>
+ </button>
+ <button type="button" onclick={() => copy(secret)} class="btn">
+ <iconify-icon icon="ph:copy-bold" width="32"></iconify-icon>
+ </button>
+ </div>
+ </div>
+ </div>
+ {/if}
+
+ <div class="space-y-2">
+ <button type="submit" class="btn btn-accent">{$t("payments.submit")}</button
+ >
+ {#if pubkey}
+ <button type="button" class="btn" onclick={del}>
+ <iconify-icon icon="ph:trash-bold" width="32"></iconify-icon>
+ {$t("accounts.delete")}</button
+ >
+ {/if}
+ </div>
+</form>
+
+{#if settingBudget}
+ <div
+ class="fixed bg-base-100 bg-opacity-90 inset-0 overflow-y-auto h-full w-full z-50 max-w-lg mx-auto"
+ >
+ <div class="relative p-5 border shadow-lg rounded-md bg-base-100 space-y-5">
+ <h1 class="text-center text-2xl font-semibold">
+ {$t("accounts.budget")}
+ </h1>
+ <Numpad
+ bind:amount={max_amount}
+ {currency}
+ {rate}
+ bind:submit={done}
+ bind:element={numpad}
+ />
+
+ <button bind:this={done} type="button" onclick={setBudget} class="btn"
+ >Ok</button
+ >
+ </div>
+ </div>
+{/if}
diff --git a/src/locales/en.json b/src/locales/en.json
index 27736963..1725abf0 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -4,6 +4,25 @@
"notFound": "No notes found"
},
"accounts": {
+ "per": "per",
+ "none": "None",
+ "day": "day",
+ "week": "week",
+ "month": "month",
+ "year": "year",
+ "daily": "Daily",
+ "weekly": "Weekly",
+ "monthly": "Monthly",
+ "yearly": "Yearly",
+ "never": "Never",
+ "newConection": "New connection",
+ "name": "Name",
+ "pubkey": "Pubkey",
+ "secret": "Secret",
+ "connection": "Connection",
+ "budget": "Budget",
+ "delete": "Delete",
+ "renewal": "Renewal period",
"showQR": "Show QR",
"revealNwc": "Reveal connection string",
"finish": "Finish",
@@ -206,7 +225,7 @@
"moreDetails": "Profile details",
"noMessages": "You have not sent or received any messages",
"profilePage": "Profile page",
- "nwc": "Nostr wallet connect",
+ "nwc": "Nostr wallet connections",
"nostrPubkey": "Nostr pubkey",
"requestPayment": "Request Payment",
"welcome": "Welcome! Your account balance is empty. You can start by making a deposit or by asking someone to pay you.",
@@ -257,6 +276,7 @@
"placeholder": "Enter an address, invoice or username"
},
"settings": {
+ "name": "Name",
"integrations": "Integrations",
"syncWithExtension": "Sync with extension",
"connectNostr": "Nostr Wallet Connect",
@@ -285,7 +305,7 @@
"nostrKeys": "Nostr secret key",
"nostrDescription": "Your nostr secret key is used to sign and decrypt messages, keep it safe or someone may be able to impersonate you.",
"nwc": "Nostr wallet connect",
- "nwcDescription": "Enable your nostr app to send payments from your account",
+ "nwcDescription": "Enable nostr apps to send payments from your account",
"seedDescription": "This 12 word seed phrase is used to derive your nostr private key and your self-custodial wallet keys which determine your auto-withdrawal addresses. Back it up and don't let anyone see it.",
"emailDescription": "For password resets and payment notifications only",
"hideNsec": "Hide nostr secret key",
diff --git a/src/routes/(app)/apps/[pubkey]/+page.server.ts b/src/routes/(app)/apps/[pubkey]/+page.server.ts
new file mode 100644
index 00000000..a2cc50e7
--- /dev/null
+++ b/src/routes/(app)/apps/[pubkey]/+page.server.ts
@@ -0,0 +1,26 @@
+import getRates from "$lib/rates";
+import { auth, fd, get, post } from "$lib/utils";
+import { fail, redirect } from "@sveltejs/kit";
+
+export const load = async ({ cookies, params, parent }) => {
+ const { pubkey } = params;
+ const { user } = await parent();
+ const rates = await getRates();
+ const rate = rates[user.currency];
+ const app = await get(`/app/${pubkey}`, auth(cookies));
+ return { app, rate };
+};
+
+export const actions = {
+ default: async ({ cookies, request }) => {
+ try {
+ const body = await fd(request);
+ await post("/app", body, auth(cookies));
+ } catch (e) {
+ const { message: error } = e as Error;
+ return fail(400, { error });
+ }
+
+ redirect(307, "/settings/nostr");
+ },
+};
diff --git a/src/routes/(app)/apps/[pubkey]/+page.svelte b/src/routes/(app)/apps/[pubkey]/+page.svelte
new file mode 100644
index 00000000..d1a5f2bd
--- /dev/null
+++ b/src/routes/(app)/apps/[pubkey]/+page.svelte
@@ -0,0 +1,19 @@
+<script>
+ import { t } from "$lib/translations";
+ import AppForm from "$comp/AppForm.svelte";
+ import { untrack } from "svelte";
+ let { data, form } = $props();
+ let { app, rate, user } = $derived(data);
+</script>
+
+<div class="container px-4 max-w-lg mx-auto space-y-5 mt-20">
+ <h2 class="text-2xl font-bold mb-2">{$t("accounts.connection")}</h2>
+
+ {#if form?.error}
+ <div class="text-red-600 text-center">
+ {form.error}
+ </div>
+ {/if}
+
+ <AppForm {...app} {rate} {user} />
+</div>
diff --git a/src/routes/(app)/apps/new/+page.server.ts b/src/routes/(app)/apps/new/+page.server.ts
new file mode 100644
index 00000000..7c335f8c
--- /dev/null
+++ b/src/routes/(app)/apps/new/+page.server.ts
@@ -0,0 +1,31 @@
+import getRates from "$lib/rates";
+import { auth, fd, post } from "$lib/utils";
+import { fail, redirect } from "@sveltejs/kit";
+
+export const load = async ({ parent, url }) => {
+ const app = {
+ name: url.searchParams.get("name"),
+ pubkey: url.searchParams.get("pubkey"),
+ max_amount: url.searchParams.get("max_amount"),
+ budget_renewal: url.searchParams.get("budget_renewal"),
+ };
+
+ const { user } = await parent();
+ const rates = await getRates();
+ const rate = rates[user.currency];
+ return { app, rate };
+};
+
+export const actions = {
+ default: async ({ cookies, request }) => {
+ try {
+ const body = await fd(request);
+ await post("/app", body, auth(cookies));
+ } catch (e) {
+ const { message: error } = e as Error;
+ return fail(400, { error });
+ }
+
+ redirect(307, "/settings/nostr");
+ },
+};
diff --git a/src/routes/(app)/apps/new/+page.svelte b/src/routes/(app)/apps/new/+page.svelte
new file mode 100644
index 00000000..d1a5f2bd
--- /dev/null
+++ b/src/routes/(app)/apps/new/+page.svelte
@@ -0,0 +1,19 @@
+<script>
+ import { t } from "$lib/translations";
+ import AppForm from "$comp/AppForm.svelte";
+ import { untrack } from "svelte";
+ let { data, form } = $props();
+ let { app, rate, user } = $derived(data);
+</script>
+
+<div class="container px-4 max-w-lg mx-auto space-y-5 mt-20">
+ <h2 class="text-2xl font-bold mb-2">{$t("accounts.connection")}</h2>
+
+ {#if form?.error}
+ <div class="text-red-600 text-center">
+ {form.error}
+ </div>
+ {/if}
+
+ <AppForm {...app} {rate} {user} />
+</div>
diff --git a/src/routes/(app)/qr/[text]/+page.server.ts b/src/routes/(app)/qr/[text]/+page.server.ts
index 5a9debb3..210c3c45 100644
--- a/src/routes/(app)/qr/[text]/+page.server.ts
+++ b/src/routes/(app)/qr/[text]/+page.server.ts
@@ -1,3 +1,4 @@
export async function load({ params }) {
- return params;
+ const { text } = params;
+ return { text };
}
diff --git a/src/routes/(app)/settings/nostr/+page.server.ts b/src/routes/(app)/settings/nostr/+page.server.ts
index b4bb124b..1767ebaf 100644
--- a/src/routes/(app)/settings/nostr/+page.server.ts
+++ b/src/routes/(app)/settings/nostr/+page.server.ts
@@ -1,9 +1,10 @@
-import { get } from "$lib/utils";
import updateUser from "$lib/settings";
+import { auth, get } from "$lib/utils";
-export const load = async () => {
+export const load = async ({ cookies }) => {
const { challenge } = await get("/challenge");
- return { challenge };
+ const apps = await get("/apps", auth(cookies));
+ return { apps, challenge };
};
export const actions = {
diff --git a/src/routes/(app)/settings/nostr/+page.svelte b/src/routes/(app)/settings/nostr/+page.svelte
index 20c18c6f..6b4c2f12 100644
--- a/src/routes/(app)/settings/nostr/+page.svelte
+++ b/src/routes/(app)/settings/nostr/+page.svelte
@@ -1,10 +1,11 @@
<script>
+ import { untrack } from "svelte";
import { browser } from "$app/environment";
import { getNsec } from "$lib/nostr";
import { page } from "$app/stores";
import { tick } from "svelte";
import { t } from "$lib/translations";
- import { copy, fail } from "$lib/utils";
+ import { s, copy, fail } from "$lib/utils";
import {
PUBLIC_COINOS_PUBKEY as pk,
PUBLIC_COINOS_RELAY as relay,
@@ -12,9 +13,10 @@
import { signer, save } from "$lib/store";
let { data } = $props();
- let { challenge, user } = $derived(data);
- let { npub } = $state(user);
+ let { apps, challenge, user } = $derived(data);
+ let npub = $state(untrack(() => user.npub));
let extensionAvailable = $derived(browser && window.nostr);
+ let { locale } = $derived(user);
let newNsec = $state(),
nsec = $state(),
@@ -31,14 +33,6 @@
}
};
- let lud16 = $derived(`${user.username}@${$page.url.host}`);
- let nwc = $derived(
- `nostr+walletconnect://${pk}?relay=${encodeURIComponent(
- relay,
- )}&secret=${user.nwc}&lud16=${lud16}`,
- );
- let toggleNwc = () => (revealNwc = !revealNwc);
-
let extension = $state();
let getPubkey = async () => {
$signer = { method: "extension", ready: true };
@@ -47,30 +41,114 @@
await tick();
$save.click();
};
+
+ let units = (r) =>
+ ({
+ daily: $t("accounts.day"),
+ weekly: $t("accounts.week"),
+ monthly: $t("accounts.month"),
+ yearly: $t("accounts.year"),
+ })[r];
</script>
-<div>
- <input type="hidden" name="challenge" value={challenge} />
- <input type="hidden" name="extension" value={extension} />
- <label for="nwc" class="font-bold">{$t("user.settings.nwc")}</label>
+<input type="hidden" name="challenge" value={challenge} />
+<input type="hidden" name="extension" value={extension} />
- <p class="text-secondary mb-1">
+<div class="p-4">
+ <h2 class="text-2xl font-bold mb-2">
+ {$t("user.settings.nwc")}
+ </h2>
+ <p class="text-secondary mb-4">
{$t("user.settings.nwcDescription")}
</p>
- <div class="flex flex-wrap justify-center gap-2">
- <a href={nwc} class="btn grow">
- <img src="/images/nostr.png" class="w-8" />
- <div class="my-auto">{$t("user.settings.connectNostr")}</div>
- </a>
+ <div class="space-y-2">
+ {#each apps as connection, i}
+ {@const last = i === apps.length - 1}
+ <div class="p-4" class:border-b-8={!last}>
+ <div class="flex justify-center gap-2 items-center p-4">
+ <div class="grow text-xl break-words min-w-0">{connection.name}</div>
+ {#if connection.max_amount > 0}
+ <div class="flex text-xl gap-1">
+ <div class="flex items-center font-bold">
+ <iconify-icon
+ noobserver
+ icon="ph:lightning-fill"
+ class="text-yellow-300"
+ ></iconify-icon>
+ {s(connection.max_amount, locale)}
+ </div>
- {#if !revealNwc}
- <button onclick={toggleNwc} type="button" class="btn grow">
- <iconify-icon noobserver icon="ph:warning-bold" width="32"
- ></iconify-icon>
- <div class="my-auto">{$t("accounts.revealNwc")}</div></button
- >
- {/if}
+ {#if connection.budget_renewal !== "never"}
+ <div>
+ {connection.budget_renewal}
+ </div>
+ {/if}
+ </div>
+ {/if}
+ </div>
+
+ <div class="flex flex-wrap justify-center gap-1">
+ <a
+ href={connection.nwc}
+ class="btn bg-gradient-to-tr from-purple-500 to-pink-500 !w-auto shrink text-white grow whitespace-nowrap"
+ class:btn-disabled={!connection.secret}
+ aria-label="Open nostr"
+ >
+ <iconify-icon icon="ph:arrow-square-out-bold" width="32"
+ ></iconify-icon>
+ <div>Connect</div>
+ </a>
+
+ <div class="flex gap-1 w-full">
+ <a
+ href={`/apps/${connection.pubkey}`}
+ class="btn !w-auto grow"
+ aria-label="Edit"
+ >
+ <iconify-icon icon="ph:pencil-bold" width="32"></iconify-icon>
+ </a>
+
+ <a
+ href={`/apps/${connection.pubkey}/payments`}
+ aria-label="Payments"
+ class="btn !w-auto grow"
+ class:btn-disabled={!connection.secret}
+ title={$t("accounts.copy")}
+ >
+ <iconify-icon icon="ph:clock-bold" width="32"></iconify-icon>
+ </a>
+
+ <button
+ aria-label="Copy"
+ type="button !w-auto grow"
+ onclick={() => copy(connection.nwc)}
+ class="btn !w-auto grow"
+ class:btn-disabled={!connection.secret}
+ title={$t("accounts.copy")}
+ >
+ <iconify-icon icon="ph:copy-bold" width="32"></iconify-icon>
+ </button>
+
+ <a
+ aria-label="QR"
+ href={`/qr/${encodeURIComponent(connection.nwc)}`}
+ class="btn !w-auto grow"
+ class:btn-disabled={!connection.secret}
+ title={$t("user.receive.showQR")}
+ disabled={!connection.secret}
+ >
+ <iconify-icon icon="ph:qr-code-bold" width="32"></iconify-icon>
+ </a>
+ </div>
+ </div>
+ </div>
+ {/each}
+
+ <a href="/apps/new" class="btn">
+ <iconify-icon icon="ph:plus-bold" width="32"></iconify-icon>
+ {$t("accounts.newConection")}
+ </a>
</div>
</div>
@@ -93,13 +171,21 @@
<div class="space-y-2">
<div class="font-bold">{$t("user.nostrPubkey")}</div>
<div class="flex gap-4">
- <textarea name="pubkey" bind:value={npub}> </textarea>
+ <textarea name="pubkey" bind:value={npub} rows={3}> </textarea>
<div class="flex my-auto gap-1">
- <button type="button" class="my-auto" onclick={() => copy(npub)}
+ <button
+ type="button"
+ class="my-auto btn btn-circle"
+ onclick={() => copy(npub)}
+ aria-label="Copy"
><iconify-icon noobserver icon="ph:copy-bold" width="32"
></iconify-icon></button
>
- <a href={`/qr/${encodeURIComponent(npub)}`} class="my-auto">
+ <a
+ href={`/qr/${encodeURIComponent(npub)}`}
+ class="my-auto btn btn-circle"
+ aria-label="QR"
+ >
<iconify-icon noobserver icon="ph:qr-code-bold" width="32"
></iconify-icon>
</a>
@@ -108,8 +194,7 @@
{#if extensionAvailable}
<button class="btn" type="button" onclick={getPubkey}>
- <iconify-icon icon="lucide-lab:bee" width="32" class="text-yellow-400"
- ></iconify-icon>
+ <img src="/images/alby.svg" width="32" />
{$t("user.settings.syncWithExtension")}</button
>
{/if}
diff --git a/static/images/alby.svg b/static/images/alby.svg
new file mode 100644
index 00000000..db5b799d
--- /dev/null
+++ b/static/images/alby.svg
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ width="372.86301"
+ height="296.45599"
+ fill="none"
+ version="1.1"
+ id="svg6"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <defs
+ id="defs6" />
+ <circle
+ cx="-36.674999"
+ cy="36.674999"
+ r="36.674999"
+ fill="#000000"
+ transform="scale(-1,1)"
+ id="circle1" />
+ <path
+ stroke="#000000"
+ stroke-width="18.337"
+ d="m 30.562998,31.173 68.46,68.46"
+ id="path1" />
+ <circle
+ cx="336.18799"
+ cy="36.675007"
+ r="36.674999"
+ fill="#000000"
+ id="circle2" />
+ <path
+ stroke="#000000"
+ stroke-width="18.337"
+ d="m 342.911,31.173 -68.46,68.46"
+ id="path2" />
+ <path
+ fill="#ffdf6f"
+ fill-rule="evenodd"
+ d="m 47.084998,255.525 c -19.766,-9.41 -31.27,-30.542 -27.417,-52.092 C 36.207998,110.938 105.135,41.565 187.654,41.565 c 82.719,0 151.78,69.71 168.105,162.54 3.796,21.59 -7.801,42.712 -27.635,52.045 -42.441,19.973 -89.846,31.137 -139.859,31.137 -50.532,0 -98.402002,-11.398 -141.180002,-31.762 z"
+ clip-rule="evenodd"
+ id="path3" />
+ <path
+ fill="#000000"
+ d="m 355.759,204.105 9.03,-1.588 z M 28.693998,205.047 C 44.674998,115.672 110.737,50.733 187.654,50.733 V 32.396 c -88.121002,0 -159.914002,73.808 -177.011002,169.423 l 18.05,3.228 z M 187.654,50.733 c 77.102,0 143.3,65.256 159.075,154.96 l 18.06,-3.176 C 347.914,106.56 275.989,32.396 187.654,32.396 Z M 324.22,247.854 c -41.246,19.41 -87.32,30.264 -135.955,30.264 v 18.338 c 51.39,0 100.126,-11.475 143.763,-32.01 z m -135.955,30.264 c -49.14,0 -95.666002,-11.081 -137.239002,-30.872 l -7.882,16.557 c 43.983,20.939 93.197002,32.653 145.121002,32.653 z m 158.464,-72.425 c 3.044,17.31 -6.224,34.498 -22.509,42.161 l 7.808,16.592 c 23.384,-11.004 37.31,-36.059 32.761,-61.929 z M 10.642998,201.819 c -4.6179995,25.823 9.198,50.891 32.501,61.984 l 7.882,-16.557 c -16.23,-7.726 -25.422,-24.922 -22.332,-42.199 z"
+ id="path4" />
+ <path
+ fill="#000000"
+ fill-rule="evenodd"
+ d="m 82.353998,232.53 c -15.911,-6.48 -25.349,-23.409 -19.836,-39.681 17.002,-50.179 66.600002,-86.492 125.136002,-86.492 58.536,0 108.134,36.313 125.137,86.492 5.513,16.272 -3.925,33.201 -19.836,39.681 -32.498,13.235 -68.048,20.527 -105.301,20.527 -37.253,0 -72.803,-7.292 -105.300002,-20.527 z"
+ clip-rule="evenodd"
+ id="path5" />
+ <ellipse
+ cx="229.82999"
+ cy="184.597"
+ fill="#ffffff"
+ rx="30.563"
+ ry="24.450001"
+ id="ellipse5" />
+ <ellipse
+ cx="142.338"
+ cy="184.61198"
+ fill="#ffffff"
+ rx="30.563"
+ ry="24.450001"
+ id="ellipse6" />
+</svg>