aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2025-07-10 12:36:50 -0700
committerAdam Soltys <[email protected]>2025-07-10 12:36:50 -0700
commit5b458624745c7ea80d1e63c9a6ef77075763ff5f (patch)
tree462e71c86d97ed78eaf5324697350feab8af6a33
parentee8e20ce1dbcc0476ec698dd8c9d0ff99e260db7 (diff)
downloadcoinos-ui-5b458624745c7ea80d1e63c9a6ef77075763ff5f.tar.gz
coinos-ui-5b458624745c7ea80d1e63c9a6ef77075763ff5f.zip
user onboarding
-rw-r--r--android/app/src/main/java/io/coinos/MainActivity.java5
-rw-r--r--android/app/src/main/java/io/coinos/nfcwriter/NfcWriterPlugin.java62
-rw-r--r--capacitor.config.json7
-rw-r--r--src/lib/utils.ts2
-rw-r--r--src/locales/en.json7
-rw-r--r--src/routes/(app)/+layout.svelte45
-rw-r--r--src/routes/(app)/[username]/(profile)/+page.svelte22
-rw-r--r--src/routes/(app)/fund/[id]/sweep/+page.server.ts10
-rw-r--r--src/routes/(app)/fund/[id]/sweep/+page.svelte6
-rw-r--r--src/routes/(app)/funder/+page.svelte55
-rw-r--r--src/routes/(app)/invoice/[id]/tip/+page.server.ts28
-rw-r--r--src/routes/(app)/settings/profile/+page.svelte16
-rw-r--r--src/routes/(app)/settings/security/+page.svelte12
13 files changed, 225 insertions, 52 deletions
diff --git a/android/app/src/main/java/io/coinos/MainActivity.java b/android/app/src/main/java/io/coinos/MainActivity.java
index 501d2e0f..271ee363 100644
--- a/android/app/src/main/java/io/coinos/MainActivity.java
+++ b/android/app/src/main/java/io/coinos/MainActivity.java
@@ -23,11 +23,16 @@ import android.nfc.NfcAdapter;
import java.nio.charset.Charset;
import java.util.Arrays;
+import io.coinos.nfcwriter.NfcWriterPlugin;
+
public class MainActivity extends BridgeActivity {
+ private NfcAdapter nfcAdapter;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
// ✅ Let Capacitor inflate its layout and wire everything
super.onCreate(savedInstanceState);
+ registerPlugin(NfcWriterPlugin.class);
WebView webView = bridge.getWebView(); // This works *after* super.onCreate()
diff --git a/android/app/src/main/java/io/coinos/nfcwriter/NfcWriterPlugin.java b/android/app/src/main/java/io/coinos/nfcwriter/NfcWriterPlugin.java
new file mode 100644
index 00000000..d2954e9b
--- /dev/null
+++ b/android/app/src/main/java/io/coinos/nfcwriter/NfcWriterPlugin.java
@@ -0,0 +1,62 @@
+// src/android/NfcWriterPlugin.java
+package io.coinos.nfcwriter;
+
+import android.nfc.NfcAdapter;
+import android.nfc.Tag;
+import android.nfc.NfcAdapter.ReaderCallback;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.getcapacitor.Plugin;
+import com.getcapacitor.PluginCall;
+import com.getcapacitor.PluginMethod;
+import com.getcapacitor.annotation.CapacitorPlugin;
+
+@CapacitorPlugin(name = "NfcWriter")
+public class NfcWriterPlugin extends Plugin {
+ private NfcAdapter nfcAdapter;
+
+ @Override
+ public void load() {
+ super.load();
+ nfcAdapter = NfcAdapter.getDefaultAdapter(getContext());
+ }
+
+ @PluginMethod
+ public void enableExclusiveNfcMode(PluginCall call) {
+ if (nfcAdapter == null) {
+ call.reject("NFC adapter not available");
+ return;
+ }
+
+ nfcAdapter.enableReaderMode(
+ getActivity(),
+ new ReaderCallback() {
+ @Override
+ public void onTagDiscovered(Tag tag) {
+ Log.d("NfcWriter", "Tag discovered");
+ bridge.getWebView().post(() -> {
+ bridge.getWebView().evaluateJavascript(
+ "window.dispatchEvent(new CustomEvent('nativeNfcTagDiscovered'))",
+ null
+ );
+ });
+ }
+ },
+ NfcAdapter.FLAG_READER_NFC_A |
+ NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK |
+ NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS,
+ null
+ );
+
+ call.resolve();
+ }
+
+ @PluginMethod
+ public void disableExclusiveNfcMode(PluginCall call) {
+ if (nfcAdapter != null) {
+ nfcAdapter.disableReaderMode(getActivity());
+ }
+ call.resolve();
+ }
+}
diff --git a/capacitor.config.json b/capacitor.config.json
index a31c19db..99458def 100644
--- a/capacitor.config.json
+++ b/capacitor.config.json
@@ -2,13 +2,16 @@
"appId": "io.coinos",
"appName": "Coinos",
"server": {
- "hostname": "coinos.io",
- "url": "https://coinos.io/start"
+ "hostname": "dev.coinos.io",
+ "url": "https://dev.coinos.io/start"
},
"android": {
"webContentsDebuggingEnabled": true,
"allowMixedContent": true,
"useBrowserHistory": true,
"immersive": false
+ },
+ "plugins": {
+ "NfcWriter": {}
}
}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 31d93fde..28a219e1 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -486,5 +486,5 @@ export const register = async (user, ip, cookies, loginRedirect) => {
});
if (error) return svelteFail(400, { error });
- redirect(303, loginRedirect || `/${user.username}`);
+ redirect(303, loginRedirect || `/${user.username}`);
};
diff --git a/src/locales/en.json b/src/locales/en.json
index 922d24b3..88de69da 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -350,7 +350,12 @@
"locale": "Language",
"username": "Username",
"email": "Email",
- "newPassword": "Change Password",
+ "newPassword": "New Password",
+ "newPasswordDesc": "(Leave blank to keep unchanged)",
+ "yourUsername": "Your username is",
+ "yourPassword": "and your password is",
+ "welcome": "Welcome!",
+ "clickBelow": "Click below to finish setting up your account",
"about": "About",
"aboutPlaceholder": "A little something about you or your business address and hours",
"emailNotifications": "Email Notifications",
diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte
index af4dbf2c..830e5291 100644
--- a/src/routes/(app)/+layout.svelte
+++ b/src/routes/(app)/+layout.svelte
@@ -46,22 +46,35 @@
checkSocket();
$pin = getCookie("pin");
- if (window.NDEFReader && user.nfc) {
- try {
- $ndef = new NDEFReader();
- await $ndef.scan();
-
- $ndef.addEventListener("readingerror", (e) => {
- console.log("nfc error", e);
- });
-
- $ndef.addEventListener("reading", ({ message, serialNumber }) => {
- goto(`/send/${message}`);
- });
- } catch (error) {
- console.log("Argh! " + error);
- }
- }
+ // if (window.NDEFReader) {
+ // try {
+ // $ndef = new NDEFReader();
+ // await $ndef.scan();
+ //
+ // $ndef.addEventListener("readingerror", (e) => {
+ // console.log("nfc error", e);
+ // });
+ //
+ // $ndef.addEventListener("reading", ({ message, serialNumber }) => {
+ // console.log("MESSAGE", message);
+ //
+ // const record = message.records[0];
+ //
+ // if (record.recordType === "url") {
+ // const textDecoder = new TextDecoder();
+ // const url = textDecoder.decode(record.data);
+ // console.log("URL:", url);
+ //
+ // // Navigate to the URL or use it however you want
+ // goto(`/send/${encodeURIComponent(url)}`);
+ // } else {
+ // console.error("Unsupported record type:", record.recordType);
+ // }
+ // });
+ // } catch (error) {
+ // console.log("Argh! " + error);
+ // }
+ // }
}
});
diff --git a/src/routes/(app)/[username]/(profile)/+page.svelte b/src/routes/(app)/[username]/(profile)/+page.svelte
index d064c3a9..31320197 100644
--- a/src/routes/(app)/[username]/(profile)/+page.svelte
+++ b/src/routes/(app)/[username]/(profile)/+page.svelte
@@ -6,7 +6,7 @@
import Account from "$comp/Account.svelte";
import Balance from "$comp/Balance.svelte";
import { t } from "$lib/translations";
- import { installPrompt } from "$lib/store";
+ import { installPrompt, password } from "$lib/store";
import { afterNavigate, preloadData } from "$app/navigation";
import { page } from "$app/stores";
@@ -32,6 +32,24 @@
</script>
<div class="space-y-2">
+ {#if user?.fresh}
+ <div class="rounded-2xl space-y-2 p-4 shadow-lg">
+ <h1 class="text-3xl font-bold">{$t("user.settings.welcome")}</h1>
+ <div>
+ {$t("user.settings.yourUsername")} <b>{user?.username}</b>
+ {#if $password}
+ {$t("user.settings.yourPassword")}
+ <b>{$password}</b>{/if}
+ </div>
+
+ <div>{$t("user.settings.clickBelow")}</div>
+
+ <a href="/settings/profile" class="btn">
+ <iconify-icon icon="ph:gear-bold" width="32"></iconify-icon>
+ Settings</a
+ >
+ </div>
+ {/if}
{#if user?.id && user.id === subject.id}
<div class="space-y-5" data-sveltekit-preload-data="false">
{#each accounts as account, i}
@@ -55,6 +73,8 @@
{/if}
</div>
+<!-- <a href="/funder" class="btn">Funder</a> -->
+
<div class="fixed inset-x-0 mx-auto flex bottom-16 px-4">
{#if user?.username !== subject.username && (!subject.anon || subject.lud16)}
<a
diff --git a/src/routes/(app)/fund/[id]/sweep/+page.server.ts b/src/routes/(app)/fund/[id]/sweep/+page.server.ts
index 3e0b8dad..d8415ca4 100644
--- a/src/routes/(app)/fund/[id]/sweep/+page.server.ts
+++ b/src/routes/(app)/fund/[id]/sweep/+page.server.ts
@@ -10,18 +10,22 @@ export async function load({ cookies, request, params, parent }) {
const rates = await getRates();
const rate = rates[user?.currency || "USD"];
+ const username = randomName();
+ const password = randomPassword();
+
if (!amount) redirect(307, `/fund/${id}`);
if (!user) {
const ip = request.headers.get("cf-connecting-ip");
user = {
- username: randomName(),
- password: randomPassword(),
+ username,
+ password,
+ fresh: true,
};
await register(user, ip, cookies, `/fund/${id}/sweep`);
}
await post("/take", { amount, id }, auth(cookies));
- return { amount, id, rate, rates };
+ return { amount, id, rate, rates, password };
}
diff --git a/src/routes/(app)/fund/[id]/sweep/+page.svelte b/src/routes/(app)/fund/[id]/sweep/+page.svelte
index 986a84bb..adba77ae 100644
--- a/src/routes/(app)/fund/[id]/sweep/+page.svelte
+++ b/src/routes/(app)/fund/[id]/sweep/+page.svelte
@@ -1,4 +1,6 @@
<script>
+ import { onMount } from "svelte";
+ import { password } from "$lib/store";
import Amount from "$comp/Amount.svelte";
import Success from "$comp/Success.svelte";
import { scale } from "svelte/transition";
@@ -9,9 +11,11 @@
let { data } = $props();
- let { amount, id, user, rate, rates } = data;
+ let { amount, id, user, rate, rates, password: pw } = data;
let { currency } = user;
let locale = $derived(loc(user));
+
+ onMount(() => ($password = pw));
</script>
<div class="container px-4 text-center mx-auto">
diff --git a/src/routes/(app)/funder/+page.svelte b/src/routes/(app)/funder/+page.svelte
new file mode 100644
index 00000000..e6549360
--- /dev/null
+++ b/src/routes/(app)/funder/+page.svelte
@@ -0,0 +1,55 @@
+<script lang="ts">
+ import { onMount } from 'svelte';
+let here = false;
+ let ready = false;
+ let success = false;
+ let controller: AbortController | null = null;
+ let NfcWriter: any;
+
+ async function write() {
+ NfcWriter = window.Capacitor?.Plugins?.NfcWriter;
+ ready = true;
+ console.log(window.Capacitor);
+ if (!NfcWriter) return;
+ here = true;
+
+ try {
+ await NfcWriter.enableExclusiveNfcMode();
+
+ if (controller) controller.abort();
+ controller = new AbortController();
+
+ ready = true;
+
+ const ndef = new NDEFReader();
+ await ndef.write(
+ {
+ records: [
+ {
+ recordType: 'url',
+ data: 'https://coinos.io',
+ },
+ ],
+ },
+ { signal: controller.signal }
+ );
+
+ success = true;
+ } catch (err) {
+ if (err.name === 'AbortError') {
+ console.warn('Previous write aborted');
+ } else {
+ console.error('Write failed', err);
+ alert('❌ Write failed: ' + err.message);
+ }
+ } finally {
+ await NfcWriter.disableExclusiveNfcMode();
+ }
+ }
+</script>
+
+HI
+<button class="p-4 btn" on:click={write}>write</button>
+{#if here}HERE{/if}
+{#if ready}go{/if}
+{#if success}done{/if}
diff --git a/src/routes/(app)/invoice/[id]/tip/+page.server.ts b/src/routes/(app)/invoice/[id]/tip/+page.server.ts
index de24c639..286d7c35 100644
--- a/src/routes/(app)/invoice/[id]/tip/+page.server.ts
+++ b/src/routes/(app)/invoice/[id]/tip/+page.server.ts
@@ -8,20 +8,22 @@ export async function load({ cookies, parent }) {
let { invoice, subject, user } = await parent();
if (!invoice.amount) redirect(307, `/invoice/${invoice.id}`);
- const trust = await get("/trust", auth(cookies));
- const trusted = trust.includes(invoice.uid);
- if (trusted) {
- if (invoice.prompt && invoice.tip === null && user.tip > 0) {
- invoice.tip = Math.round(invoice.amount * (user.tip / 100));
- invoice = await post(
- "/invoice",
- { invoice, user: subject },
- auth(cookies),
- );
- }
+ if (user) {
+ const trust = await get("/trust", auth(cookies));
+ const trusted = trust.includes(invoice.uid);
+ if (trusted) {
+ if (invoice.prompt && invoice.tip === null && user.tip > 0) {
+ invoice.tip = Math.round(invoice.amount * (user.tip / 100));
+ invoice = await post(
+ "/invoice",
+ { invoice, user: subject },
+ auth(cookies),
+ );
+ }
- const p = await post("/payments", invoice, auth(cookies));
- redirect(307, `/sent/${p.id}`);
+ const p = await post("/payments", invoice, auth(cookies));
+ redirect(307, `/sent/${p.id}`);
+ }
}
const rate = rates[subject?.currency];
diff --git a/src/routes/(app)/settings/profile/+page.svelte b/src/routes/(app)/settings/profile/+page.svelte
index bee50d39..ec9855d0 100644
--- a/src/routes/(app)/settings/profile/+page.svelte
+++ b/src/routes/(app)/settings/profile/+page.svelte
@@ -3,16 +3,18 @@
import { avatar, banner as bannerStore } from "$lib/store";
import { t } from "$lib/translations";
import { page } from "$app/stores";
+ import PasswordInput from "$comp/PasswordInput.svelte";
let { data } = $props();
let { user } = $derived(data);
let { id } = user;
- let { about, banner, picture, display, username } =
- $state(user);
+ let { about, banner, picture, display, username } = $state(user);
let avatarFile,
avatarInput = $state(),
bannerFile,
+ password = $state(),
+ revealPassword = $state(),
bannerInput = $state();
let selectAvatar = () => avatarInput.click();
@@ -64,6 +66,16 @@
</div>
</div>
+<input type="hidden" name="confirm" bind:value={password} />
+
+<div>
+ <label for="password" class="block font-bold block mb-1"
+ >{$t("user.settings.newPassword")}</label
+ >
+
+ <PasswordInput bind:value={password} placeholder="(Leave blank to keep unchanged)" />
+</div>
+
<div>
<label for="display" class="font-bold mb-1 block"
>{$t("user.settings.displayName")}</label
diff --git a/src/routes/(app)/settings/security/+page.svelte b/src/routes/(app)/settings/security/+page.svelte
index ea1bf1f9..8c38bca1 100644
--- a/src/routes/(app)/settings/security/+page.svelte
+++ b/src/routes/(app)/settings/security/+page.svelte
@@ -4,7 +4,6 @@
import { t } from "$lib/translations";
import Pin from "$comp/Pin.svelte";
import Qr from "$comp/Qr.svelte";
- import PasswordInput from "$comp/PasswordInput.svelte";
import { copy, post, success, fail } from "$lib/utils";
import { save, pin as current } from "$lib/store";
import { invalidate } from "$app/navigation";
@@ -21,9 +20,7 @@
newNsec = $state(),
nsec = $state(),
otp = $state(),
- password = $state(),
pin = $state(""),
- revealPassword = $state(),
revealSeed,
revealNsec = $state(),
token = $state(""),
@@ -157,15 +154,6 @@
</script>
<input type="hidden" name="newpin" value={disablingPin ? "delete" : pin} />
-<input type="hidden" name="confirm" bind:value={password} />
-
-<div>
- <label for="password" class="block font-bold block mb-1"
- >{$t("user.settings.newPassword")}</label
- >
-
- <PasswordInput bind:value={password} />
-</div>
<div>
<span class="font-bold mb-1"