This commit is contained in:
Paul Miller
2024-01-13 12:45:58 +00:00
parent ee32e59eb7
commit 33b8190a2d
165 changed files with 5892 additions and 10147 deletions

View File

@@ -12,12 +12,14 @@ dependencies {
implementation project(':capacitor-mlkit-barcode-scanning')
implementation project(':capacitor-app')
implementation project(':capacitor-app-launcher')
implementation project(':capacitor-camera')
implementation project(':capacitor-clipboard')
implementation project(':capacitor-filesystem')
implementation project(':capacitor-haptics')
implementation project(':capacitor-share')
implementation project(':capacitor-status-bar')
implementation project(':capacitor-toast')
implementation project(':capacitor-secure-storage-plugin')
}

View File

@@ -11,6 +11,9 @@ project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacito
include ':capacitor-app-launcher'
project(':capacitor-app-launcher').projectDir = new File('../node_modules/.pnpm/@capacitor+app-launcher@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app-launcher/android')
include ':capacitor-camera'
project(':capacitor-camera').projectDir = new File('../node_modules/.pnpm/@capacitor+camera@5.0.9_@capacitor+core@5.5.1/node_modules/@capacitor/camera/android')
include ':capacitor-clipboard'
project(':capacitor-clipboard').projectDir = new File('../node_modules/.pnpm/@capacitor+clipboard@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/clipboard/android')
@@ -28,3 +31,6 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/.pnpm/@c
include ':capacitor-toast'
project(':capacitor-toast').projectDir = new File('../node_modules/.pnpm/@capacitor+toast@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/toast/android')
include ':capacitor-secure-storage-plugin'
project(':capacitor-secure-storage-plugin').projectDir = new File('../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.1/node_modules/capacitor-secure-storage-plugin/android')

View File

@@ -1,28 +1,14 @@
import { expect, test } from "@playwright/test";
import { loadHome, visitSettings } from "./utils";
test.beforeEach(async ({ page }) => {
await page.goto("http://localhost:3420/");
});
test("test local encrypt", async ({ page }) => {
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Mutiny Wallet/);
// Wait for an element matching the selector to appear in DOM.
await page.waitForSelector("text=0 SATS");
console.log("Page loaded.");
// Wait for a while just to make sure we can load everything
await page.waitForTimeout(1000);
// Navigate to settings
const settingsLink = await page.getByRole("link", { name: "Settings" });
settingsLink.click();
// Wait for settings to load
await page.waitForSelector("text=Settings");
await loadHome(page);
await visitSettings(page);
// Click the "Backup" link
await page.click("text=Backup");
@@ -48,6 +34,13 @@ test("test local encrypt", async ({ page }) => {
// Click the "I wrote down the words" button
await wroteDownButton.click();
// Make sure the balance box ready light is on
await page.locator("title=READY");
// Go back to settings / change password
await visitSettings(page);
await page.click("text=Change Password");
// The header should now say "Encrypt your seed words"
await expect(page.locator("h1")).toContainText(["Encrypt your seed words"]);
@@ -56,7 +49,7 @@ test("test local encrypt", async ({ page }) => {
const passwordInput = await page.locator(`input[name='password']`);
// 2. Type the password into the input field
await passwordInput.type("test");
await passwordInput.fill("test");
// 3. Find the input field with the name "confirmPassword"
const confirmPasswordInput = await page.locator(
@@ -64,15 +57,21 @@ test("test local encrypt", async ({ page }) => {
);
// 4. Type the password into the input field
await confirmPasswordInput.type("test");
await confirmPasswordInput.fill("test");
// The "Encrypt" button should not be disabled
const encryptButton = await page.locator("button", { hasText: "Encrypt" });
await expect(encryptButton).not.toBeDisabled();
// wait 5 seconds for no reason (SADLY THIS IS IMPORTANT FOR THE TEST TO PASS)
await page.waitForTimeout(5000);
// Click the "Encrypt" button
await encryptButton.click();
// wait for a while just to see what happens
// await page.waitForTimeout(10000);
// Wait for a modal with the text "Enter your password"
await page.waitForSelector("text=Enter your password");
@@ -80,11 +79,11 @@ test("test local encrypt", async ({ page }) => {
const passwordInput2 = await page.locator(`input[name='password']`);
// Type the password into the input field
await passwordInput2.type("test");
await passwordInput2.fill("test");
// Click the "Decrypt Wallet" button
await page.click("text=Decrypt Wallet");
// Wait for an element matching the selector to appear in DOM.
await page.waitForSelector("text=0 SATS");
await page.locator(`text=0 sats`).first();
});

View File

@@ -1,5 +1,7 @@
import { expect, test } from "@playwright/test";
import { loadHome, visitSettings } from "./utils";
const SIGNET_INVITE_CODE =
"fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er";
@@ -8,24 +10,8 @@ test.beforeEach(async ({ page }) => {
});
test("fedmint join, receive, send", async ({ page }) => {
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Mutiny Wallet/);
// Wait for an element matching the selector to appear in DOM.
await page.waitForSelector("text=0 SATS");
console.log("Page loaded.");
// Wait for a while just to make sure we can load everything
await page.waitForTimeout(1000);
// Navigate to settings
const settingsLink = await page.getByRole("link", { name: "Settings" });
settingsLink.click();
// Wait for settings to load
await page.waitForSelector("text=Settings");
await loadHome(page);
await visitSettings(page);
// Click "Manage Federations" link
await page.click("text=Manage Federations");
@@ -45,11 +31,20 @@ test("fedmint join, receive, send", async ({ page }) => {
await page.goBack();
await page.goBack();
// Make sure there's a fedimint icon
await expect(page.getByRole("img", { name: "community" })).toBeVisible();
// Click the top left button (it's the profile button), a child of header
// TODO: better ARIA stuff
await page.locator(`header button`).first().click();
// Click the receive button
await page.click("text=Receive");
// Make sure there's text that says "fedimint"
await page.locator("text=fedimint").first();
// Navigate back home
await page.goBack();
// Click the fab button
await page.locator("#fab").click();
// Click the receive button in the fab
await page.locator("text=Receive").last().click();
// Expect the url to conain receive
await expect(page).toHaveURL(/.*receive/);
@@ -57,9 +52,6 @@ test("fedmint join, receive, send", async ({ page }) => {
// At least one h1 should show "0 sats"
await expect(page.locator("h1")).toContainText(["0 SATS"]);
// At least one h2 should show "0 USD"
await expect(page.locator("h2")).toContainText(["$0 USD"]);
// Type 100 into the input
await page.locator("#sats-input").pressSequentially("100");
@@ -72,11 +64,7 @@ test("fedmint join, receive, send", async ({ page }) => {
});
await expect(continueButton).not.toBeDisabled();
// Wait one second
// TODO: figure out how to not get an error without waiting
await page.waitForTimeout(1000);
continueButton.click();
await continueButton.click();
await expect(
page.getByText("Keep Mutiny open to complete the payment.")
@@ -109,21 +97,17 @@ test("fedmint join, receive, send", async ({ page }) => {
);
// Wait for an h1 to appear in the dom that says "Payment Received"
await page.waitForSelector("text=Payment Received", { timeout: 30000 });
await page.waitForSelector("text=Payment Received");
// Click the "Nice" button
await page.click("text=Nice");
// Make sure we have 100 sats in the fedimint balance
await expect(
page
.locator("div")
.filter({ hasText: /^100 eSATS$/ })
.nth(1)
).toBeVisible();
// Make sure we have 100 sats in the top balance
await page.waitForSelector("text=100 SATS");
// Now we send
await page.click("text=Send");
await page.locator("#fab").click();
await page.locator("text=Send").last().click();
// type refund@lnurl-staging.mutinywallet.com
const sendInput = await page.locator("input");
@@ -131,9 +115,8 @@ test("fedmint join, receive, send", async ({ page }) => {
await page.click("text=Continue");
// Wait two seconds (the destination doesn't show up immediately)
// TODO: figure out how to not get an error without waiting
await page.waitForTimeout(2000);
// Wait for the destination to show up
await page.waitForSelector("text=LIGHTNING");
// Type 90 into the input
await page.locator("#sats-input").fill("90");
@@ -147,8 +130,8 @@ test("fedmint join, receive, send", async ({ page }) => {
});
await expect(confirmButton).not.toBeDisabled();
confirmButton.click();
await confirmButton.click();
// Wait for an h1 to appear in the dom that says "Payment Sent"
await page.waitForSelector("text=Payment Sent", { timeout: 30000 });
await page.waitForSelector("text=Payment Sent");
});

View File

@@ -1,22 +1,11 @@
import { expect, test } from "@playwright/test";
import { test } from "@playwright/test";
import { loadHome } from "./utils";
test.beforeEach(async ({ page }) => {
await page.goto("http://localhost:3420/");
});
test("initial load", async ({ page }) => {
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Mutiny Wallet/);
await expect(page.locator("header")).toContainText(["Activity"], {
timeout: 30000
});
// Wait up to 30 seconds for an image element matching the selector to be visible
await page.waitForSelector("img[alt='lightning']", { timeout: 30000 });
// Wait for an element matching the selector to appear in DOM.
await page.waitForSelector("text=0 SATS");
console.log("Page loaded.");
await loadHome(page);
});

View File

@@ -1,49 +1,37 @@
import { expect, test } from "@playwright/test";
import { visitSettings } from "./utils";
test.beforeEach(async ({ page }) => {
await page.goto("http://localhost:3420/");
});
test("restore from seed @slow", async ({ page }) => {
// Start on the home page
await expect(page).toHaveTitle(/Mutiny Wallet/);
await page.waitForSelector("text=Welcome to the Mutiny!");
console.log("Waiting for new wallet to be created...");
await page.locator(`button:has-text('Import Existing')`).click();
// should have 100k sats on-chain
const TEST_SEED_WORDS =
"rival hood review write spoon tide orange ill opera enrich clip acoustic";
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Mutiny Wallet/);
// Wait for an element matching the selector to appear in DOM.
await page.waitForSelector("text=0 SATS");
console.log("Page loaded.");
// Wait for a while just to make sure we can load everything
await page.waitForTimeout(1000);
// Navigate to settings
const settingsLink = await page.getByRole("link", { name: "Settings" });
settingsLink.click();
// Wait for settings to load
await page.waitForSelector("text=Settings");
// Click the "Restore" link
page.click("text=Restore");
// There should be some warning text: "This will replace your existing wallet"
await expect(page.locator("p")).toContainText([
"This will replace your existing wallet"
]);
let seedWords = TEST_SEED_WORDS.split(" ");
const seedWords = TEST_SEED_WORDS.split(" ");
// Find the input field with the name "words.0"
for (let i = 0; i < 12; i++) {
const wordInput = await page.locator(`input[name='words.${i}']`);
// Type the seed words into the input field
await wordInput.type(seedWords[i]);
await wordInput.fill(seedWords[i]);
}
// There should be a button with the text "Restore" and it should not be disabled
@@ -54,33 +42,29 @@ test("restore from seed @slow", async ({ page }) => {
// A modal should pop up, click the "Confirm" button
const confirmButton = await page.locator("button", { hasText: "Confirm" });
confirmButton.click();
// Wait for the wallet to load
await page.waitForSelector("img[alt='lightning']");
await confirmButton.click();
// Eventually we should have a balance of 100k sats
await page.waitForSelector("text=100,000 SATS");
await page.locator("text=100,000 SATS");
// Now we should clean up after ourselves and delete the wallet
settingsLink.click();
// Wait for settings to load
await page.waitForSelector("text=Settings");
await visitSettings(page);
// Click the "Restore" link
page.click("text=Admin Page");
await page.click("text=Admin Page");
// Clicke the Delete Everything button
page.click("text=Delete Everything");
await page.click("text=Delete Everything");
// A modal should pop up, click the "Confirm" button
const confirmDeleteButton = await page.locator("button", {
hasText: "Confirm"
});
confirmDeleteButton.click();
// Wait for the wallet to load
// Wait for the wallet to load
await page.waitForSelector("img[alt='lightning']");
// wait 5 seconds for no reason
await page.waitForTimeout(5000);
await confirmDeleteButton.click();
await page.locator("text=Welcome to the Mutiny!");
});

View File

@@ -1,21 +1,26 @@
import { expect, test } from "@playwright/test";
import { loadHome } from "./utils";
test.beforeEach(async ({ page }) => {
await page.goto("http://localhost:3420/");
});
test("rountrip receive and send", async ({ page }) => {
// Click the receive button
await page.click("text=Receive");
await loadHome(page);
// Expect the url to conain receive
await page.locator("#fab").click();
await page.locator("text=Receive").last().click();
// Expect the url to contain receive
await expect(page).toHaveURL(/.*receive/);
// At least one h1 should show "0 sats"
await expect(page.locator("h1")).toContainText(["0 SATS"]);
// At least one h2 should show "0 USD"
await expect(page.locator("h2")).toContainText(["$0 USD"]);
// await expect(page.locator("h2")).toContainText(["$0 USD"]);
await page.waitForSelector("text=$0 USD");
// Type 100000 into the input
await page.locator("#sats-input").pressSequentially("100000");
@@ -72,7 +77,8 @@ test("rountrip receive and send", async ({ page }) => {
await page.click("text=Nice");
// Now we send
await page.click("text=Send");
await page.locator("#fab").click();
await page.locator("text=Send").click();
// In the textarea with the placeholder "bitcoin:..." type refund@lnurl-staging.mutinywallet.com
const sendInput = await page.locator("input");

View File

@@ -1,12 +1,14 @@
import { expect, Page, test } from "@playwright/test";
import { loadHome, visitSettings } from "./utils";
const routes = [
"/",
"/activity",
"/feedback",
"/gift",
"/receive",
"/scanner",
"/search",
"/send",
"/swap",
"/settings"
@@ -57,25 +59,13 @@ test.beforeEach(async ({ page }) => {
});
test("visit each route", async ({ page }) => {
// Start on the home page
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Mutiny Wallet/);
// Wait for an element matching the selector to appear in DOM.
await page.waitForSelector("text=0 SATS");
console.log("Page loaded.");
// Wait for a while just to make sure we can load everything
await page.waitForTimeout(1000);
await loadHome(page);
checklist.set("/", true);
await checkRoute(page, "/activity", "Activity", checklist);
await page.goBack();
await visitSettings(page);
// Navigate to settings
await checkRoute(page, "/settings", "Settings", checklist);
checklist.set("/settings", true);
// Mutiny+
await checkRoute(page, "/settings/plus", "Mutiny+", checklist);
@@ -146,22 +136,43 @@ test("visit each route", async ({ page }) => {
await checkRoute(page, "/settings/admin", "Secret Debug Tools", checklist);
await page.goBack();
// Go back home
await page.goBack();
// Feedback
await checkRoute(page, "/feedback", "Give us feedback!", checklist);
await page.goBack();
// Receive is covered in another test
checklist.set("/receive", true);
// Go back home
await page.goBack();
// Try the fab button
await page.locator("#fab").click();
await page.locator("text=Send").click();
await expect(page.locator("input").first()).toBeFocused();
// Send is covered in another test
checklist.set("/send", true);
await page.goBack();
// Try the fab button again
await page.locator("#fab").click();
// (There are actually two buttons with the "Receive text on first run)
await page.locator("text=Receive").last().click();
await expect(page.locator("h1").first()).toHaveText("Receive Bitcoin");
// Actual receive is covered in another test
checklist.set("/receive", true);
await page.goBack();
// Try the fab button again
await page.locator("#fab").click();
await page.locator("text=Scan").click();
// Scanner
await page.locator(`a[href='/scanner']`).first().click();
await expect(page.locator("button").first()).toHaveText("Paste Something");
await expect(
page.locator("button:has-text('Paste Something')")
).toBeVisible();
checklist.set("/scanner", true);
// Now we have to check routes that aren't linked to directly for whatever reason

27
e2e/utils.ts Normal file
View File

@@ -0,0 +1,27 @@
import { expect, Page } from "@playwright/test";
export async function loadHome(page: Page) {
// Start on the home page
await expect(page).toHaveTitle(/Mutiny Wallet/);
await page.waitForSelector("text=Welcome to the Mutiny!");
console.log("Waiting for new wallet to be created...");
await page.locator(`button:has-text('New Wallet')`).click();
await page.locator("text=Create your profile").first();
await page.locator("button:has-text('Skip for now')").click();
// Should have a balance up top now
await page.locator(`text=0 sats`).first();
// Status light should be ready
await page.locator(`title="READY"`).first();
}
export async function visitSettings(page: Page) {
// Find an image with an alt text of "mutiny" and click it
// TODO: probably should have better ARIA stuff for this
await page.locator("img[alt='mutiny']").first().click();
await expect(page.locator("h1").first()).toHaveText("Settings");
}

View File

@@ -47,6 +47,9 @@
#no-script {
margin: 1rem;
}
body {
background-color: hsla(0, 0%, 5%, 1);
}
</style>
</head>
<body>
@@ -63,8 +66,10 @@
Please update or enable WebAssembly to run this app.
</p>
<p>
If you're running iOS in lockdown mode you'll need to <a href="https://support.apple.com/en-us/HT212650">add an
exception for Mutiny Wallet.</a>
If you're running iOS in lockdown mode you'll need to
<a href="https://support.apple.com/en-us/HT212650"
>add an exception for Mutiny Wallet.</a
>
</p>
</div>
<noscript>
@@ -82,7 +87,7 @@
</p>
</div>
</noscript>
<div id="root"></div>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
<script>
// Check for WebAssembly support

View File

@@ -14,12 +14,14 @@ def capacitor_pods
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@5.3.0_@capacitor+core@5.5.1/node_modules/@capacitor-mlkit/barcode-scanning'
pod 'CapacitorApp', :path => '../../node_modules/.pnpm/@capacitor+app@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app'
pod 'CapacitorAppLauncher', :path => '../../node_modules/.pnpm/@capacitor+app-launcher@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app-launcher'
pod 'CapacitorCamera', :path => '../../node_modules/.pnpm/@capacitor+camera@5.0.9_@capacitor+core@5.5.1/node_modules/@capacitor/camera'
pod 'CapacitorClipboard', :path => '../../node_modules/.pnpm/@capacitor+clipboard@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/clipboard'
pod 'CapacitorFilesystem', :path => '../../node_modules/.pnpm/@capacitor+filesystem@5.1.4_@capacitor+core@5.5.1/node_modules/@capacitor/filesystem'
pod 'CapacitorHaptics', :path => '../../node_modules/.pnpm/@capacitor+haptics@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/haptics'
pod 'CapacitorShare', :path => '../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/share'
pod 'CapacitorStatusBar', :path => '../../node_modules/.pnpm/@capacitor+status-bar@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/status-bar'
pod 'CapacitorToast', :path => '../../node_modules/.pnpm/@capacitor+toast@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/toast'
pod 'CapacitorSecureStoragePlugin', :path => '../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.1/node_modules/capacitor-secure-storage-plugin'
end
target 'App' do

View File

@@ -5,6 +5,8 @@ PODS:
- Capacitor
- CapacitorAppLauncher (5.0.6):
- Capacitor
- CapacitorCamera (5.0.9):
- Capacitor
- CapacitorClipboard (5.0.6):
- Capacitor
- CapacitorCordova (5.5.1)
@@ -15,6 +17,9 @@ PODS:
- CapacitorMlkitBarcodeScanning (5.3.0):
- Capacitor
- GoogleMLKit/BarcodeScanning (= 4.0.0)
- CapacitorSecureStoragePlugin (0.9.0):
- Capacitor
- SwiftKeychainWrapper
- CapacitorShare (5.0.6):
- Capacitor
- CapacitorStatusBar (5.0.6):
@@ -75,16 +80,19 @@ PODS:
- nanopb/decode (2.30909.0)
- nanopb/encode (2.30909.0)
- PromisesObjC (2.3.1)
- SwiftKeychainWrapper (4.0.1)
DEPENDENCIES:
- "Capacitor (from `../../node_modules/.pnpm/@capacitor+ios@5.5.1_@capacitor+core@5.5.1/node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/.pnpm/@capacitor+app@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app`)"
- "CapacitorAppLauncher (from `../../node_modules/.pnpm/@capacitor+app-launcher@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app-launcher`)"
- "CapacitorCamera (from `../../node_modules/.pnpm/@capacitor+camera@5.0.9_@capacitor+core@5.5.1/node_modules/@capacitor/camera`)"
- "CapacitorClipboard (from `../../node_modules/.pnpm/@capacitor+clipboard@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/clipboard`)"
- "CapacitorCordova (from `../../node_modules/.pnpm/@capacitor+ios@5.5.1_@capacitor+core@5.5.1/node_modules/@capacitor/ios`)"
- "CapacitorFilesystem (from `../../node_modules/.pnpm/@capacitor+filesystem@5.1.4_@capacitor+core@5.5.1/node_modules/@capacitor/filesystem`)"
- "CapacitorHaptics (from `../../node_modules/.pnpm/@capacitor+haptics@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/haptics`)"
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@5.3.0_@capacitor+core@5.5.1/node_modules/@capacitor-mlkit/barcode-scanning`)"
- "CapacitorSecureStoragePlugin (from `../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.1/node_modules/capacitor-secure-storage-plugin`)"
- "CapacitorShare (from `../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/share`)"
- "CapacitorStatusBar (from `../../node_modules/.pnpm/@capacitor+status-bar@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/status-bar`)"
- "CapacitorToast (from `../../node_modules/.pnpm/@capacitor+toast@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/toast`)"
@@ -103,6 +111,7 @@ SPEC REPOS:
- MLKitVision
- nanopb
- PromisesObjC
- SwiftKeychainWrapper
EXTERNAL SOURCES:
Capacitor:
@@ -111,6 +120,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/.pnpm/@capacitor+app@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app"
CapacitorAppLauncher:
:path: "../../node_modules/.pnpm/@capacitor+app-launcher@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/app-launcher"
CapacitorCamera:
:path: "../../node_modules/.pnpm/@capacitor+camera@5.0.9_@capacitor+core@5.5.1/node_modules/@capacitor/camera"
CapacitorClipboard:
:path: "../../node_modules/.pnpm/@capacitor+clipboard@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/clipboard"
CapacitorCordova:
@@ -121,6 +132,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/.pnpm/@capacitor+haptics@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/haptics"
CapacitorMlkitBarcodeScanning:
:path: "../../node_modules/.pnpm/@capacitor-mlkit+barcode-scanning@5.3.0_@capacitor+core@5.5.1/node_modules/@capacitor-mlkit/barcode-scanning"
CapacitorSecureStoragePlugin:
:path: "../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.1/node_modules/capacitor-secure-storage-plugin"
CapacitorShare:
:path: "../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.1/node_modules/@capacitor/share"
CapacitorStatusBar:
@@ -132,11 +145,13 @@ SPEC CHECKSUMS:
Capacitor: 9da0a2415e3b6098511f8b5ffdb578d91ee79f8f
CapacitorApp: 024e1b1bea5f883d79f6330d309bc441c88ad04a
CapacitorAppLauncher: 5a9f06c13c6b4f5d65b550a07128ef04f3a216b3
CapacitorCamera: 4892c5c392f60039d853dde78bc50ba19fbd113e
CapacitorClipboard: 77edf49827ea21da2a9c05c690a4a6a4d07199c4
CapacitorCordova: e128cc7688c070ca0bfa439898a5f609da8dbcfe
CapacitorFilesystem: af704badfbc69f6f8623d9ed313e5490e3723dcb
CapacitorHaptics: 1fffc1217c7e64a472d7845be50fb0c2f7d4204c
CapacitorMlkitBarcodeScanning: ea08ef246e5d3511d5a231a59fae36b16ad9acb3
CapacitorSecureStoragePlugin: e91d7df060f2495a1acff9583641a6953e3aacba
CapacitorShare: cd41743331cb71d217c029de54b681cbd91e0fcc
CapacitorStatusBar: 565c0a1ebd79bb40d797606a8992b4a105885309
CapacitorToast: bb0d79b78d9c27c0199b57f735dd50b8fc363489
@@ -152,7 +167,8 @@ SPEC CHECKSUMS:
MLKitVision: 8baa5f46ee3352614169b85250574fde38c36f49
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
SwiftKeychainWrapper: 807ba1d63c33a7d0613288512399cd1eda1e470c
PODFILE CHECKSUM: 0a4556c3ff620e7baf2e9aeba1f6e9f4fe406a0f
PODFILE CHECKSUM: 49918019bef63fe6c45a1c0a4748457e705212b5
COCOAPODS: 1.14.2
COCOAPODS: 1.14.3

View File

@@ -19,25 +19,23 @@
"@capacitor/cli": "^5.5.1",
"@ianvs/prettier-plugin-sort-imports": "^4.1.1",
"@playwright/test": "^1.39.0",
"@types/node": "^20.8.10",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"autoprefixer": "^10.4.16",
"esbuild": "^0.14.54",
"eslint": "^8.52.0",
"eslint-import-resolver-typescript": "2.7.1",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-solid": "0.13.0",
"postcss": "^8.4.31",
"eslint": "^8.56.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-solid": "0.13.1",
"postcss": "^8.4.35",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.6",
"tailwindcss": "^3.3.5",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"prettier-plugin-tailwindcss": "^0.5.12",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.1.1",
"vite-plugin-pwa": "^0.16.7",
"vite-plugin-solid": "^2.7.0",
"vite-plugin-wasm": "^3.2.2",
"vite-plugin-solid": "^2.10.1",
"vite-plugin-wasm": "^3.3.0",
"workbox-window": "^7.0.0"
},
"dependencies": {
@@ -54,19 +52,21 @@
"@capacitor/share": "^5.0.6",
"@capacitor/status-bar": "^5.0.6",
"@capacitor/toast": "^5.0.6",
"@kobalte/core": "^0.9.8",
"@kobalte/tailwindcss": "^0.5.0",
"@modular-forms/solid": "^0.18.1",
"@mutinywallet/mutiny-wasm": "0.5.10",
"@mutinywallet/waila-wasm": "^0.2.6",
"@kobalte/core": "^0.12.1",
"@kobalte/tailwindcss": "^0.9.0",
"@mutinywallet/mutiny-wasm": "0.6.0-rc3",
"@modular-forms/solid": "^0.20.0",
"@solid-primitives/upload": "^0.0.111",
"@solidjs/meta": "^0.29.1",
"@solidjs/router": "^0.9.0",
"@solidjs/meta": "^0.29.3",
"@solidjs/router": "^0.10.9",
"capacitor-secure-storage-plugin": "^0.9.0",
"i18next": "^23.10.1",
"i18next-browser-languagedetector": "^7.1.0",
"lucide-solid": "^0.330.0",
"qr-scanner": "^1.4.2",
"solid-js": "^1.8.5",
"solid-qr-code": "^0.0.8"
"solid-js": "^1.8.12",
"solid-qr-code": "^0.0.8",
"solid-transition-group": "^0.2.3"
},
"engines": {
"node": ">=18"

8630
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

7
postcss.config.cjs Normal file
View File

@@ -0,0 +1,7 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -33,9 +33,16 @@
"find": "Find your friends on nostr",
"backup": "Secure your funds!",
"connection": "Create a wallet connection",
"federation": "Join a federation"
"connection_edit": "Edit wallet connections",
"federation": "Join a federation",
"subnav": {
"just_me": "Just Me",
"friends": "Friends",
"requests": "Requests"
}
},
"profile": {
"profile": "Profile",
"nostr_identity": "Nostr Identity",
"add_lightning_address": "Add Lightning Address",
"edit_profile": "Edit Profile"
@@ -465,7 +472,8 @@
"ios_testflight": "iOS TestFlight access",
"more": "... and more to come",
"cta_description": "Enjoy early access to new features and premium functionality.",
"cta_but_already_plus": "Thank you for your support!"
"cta_but_already_plus": "Thank you for your support!",
"lightning_address": "Your own lightning address"
},
"restore": {
"title": "Restore",
@@ -525,7 +533,7 @@
"remove": "Remove",
"expires": "Expires",
"federation_id": "Federation ID",
"description": "Mutiny has experimental support for the Fedimint protocol. You'll need a federation invite code to use this feature. Store funds in a federation at your own risk!",
"description": "Mutiny has experimental support for the Fedimint protocol. You'll need a federation invite code to use this feature. These funds are currently not backed up remotely. Store funds in a federation at your own risk!",
"learn_more": "Learn more about Fedimint."
},
"gift": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

BIN
public/mutiny-pixel-m.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

View File

@@ -1,36 +0,0 @@
// @refresh reload
import { Capacitor } from "@capacitor/core";
import { StatusBar, Style } from "@capacitor/status-bar";
import { Title } from "@solidjs/meta";
import { ErrorBoundary, Suspense } from "solid-js";
import { ErrorDisplay, I18nProvider } from "~/components";
import { Router } from "~/router";
import { Provider as MegaStoreProvider } from "~/state/megaStore";
const setStatusBarStyleDark = async () => {
await StatusBar.setStyle({ style: Style.Dark });
};
if (Capacitor.isNativePlatform()) {
await setStatusBarStyleDark();
}
export default function App() {
return (
<Suspense>
<Title>Mutiny Wallet</Title>
<ErrorBoundary fallback={(e) => <ErrorDisplay error={e} />}>
<MegaStoreProvider>
<I18nProvider>
<ErrorBoundary
fallback={(e) => <ErrorDisplay error={e} />}
>
<Router />
</ErrorBoundary>
</I18nProvider>
</MegaStoreProvider>
</ErrorBoundary>
</Suspense>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,3 +0,0 @@
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M35.597 3.477a1.5 1.5 0 0 1 .278 1.622l-11.768 27a1.5 1.5 0 0 1-2.387.509L14.157 25.7l-3.036 4.578a1.5 1.5 0 0 1-2.73-.58L6.825 20.34.872 17.596a1.5 1.5 0 0 1 .125-2.775l33-11.734a1.5 1.5 0 0 1 1.6.39ZM9.827 20.1l.896 5.35 1.902-2.869a1.5 1.5 0 0 1 .236-.276l11.04-10.122L9.826 20.1Zm8.665-8.317-13.02 4.63 2.633 1.213 10.387-5.843Zm11.805-1.395-14.2 13.02 6.098 5.57 8.102-18.59Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 531 B

View File

@@ -1,3 +0,0 @@
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.546 8 8 17.546l9.546 9.546" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 206 B

View File

@@ -1,3 +0,0 @@
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.75 27.75v-13.5a1.5 1.5 0 0 1 3 0v9.879L26.281 7.597a1.5 1.5 0 0 1 2.122 2.122L11.87 26.25h9.879a1.5 1.5 0 0 1 0 3H8.25a1.5 1.5 0 0 1-1.5-1.5Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 294 B

View File

@@ -1,3 +0,0 @@
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m14 14 20 20m-20 0 20-20" stroke="#000" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 200 B

View File

@@ -1,3 +0,0 @@
<svg width="14" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.0778 6.68331 4.44176 15.5633c-.24.246-.638-.039-.482-.345l3.074-6.06599c.02328-.04578.03442-.09677.03235-.14809-.00207-.05132-.01728-.10125-.04418-.14501-.02689-.04375-.06457-.07987-.10943-.10489-.04485-.02502-.09538-.03811-.14674-.03801H.299757c-.059058-.00005-.116788-.01752-.165955-.05024-.0491673-.03272-.087584-.07922-.1104352-.13368-.02285107-.05446-.02912021-.11445-.01802154-.17246.01109864-.058.03907164-.11144.08041244-.15362L8.09576.0913129c.232-.2349999.618.0230001.489.3280001l-2.297 5.414997c-.01945.04591-.02715.09594-.02241.14557.00475.04963.02179.0973.04958.13869.02779.04139.06546.07521.10961.09838.04414.02318.09336.03499.14322.03436l6.29104-.078c.0593-.00095.1176.01573.1675.04794.0499.03221.0891.0785.1127.133.0235.0545.0304.11477.0197.17317-.0108.0584-.0386.11231-.0799.15489l-.001.001Z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 921 B

View File

@@ -1,3 +0,0 @@
<svg width="14" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.0778 6.68331 4.44176 15.5633c-.24.246-.638-.039-.482-.345l3.074-6.06599c.02328-.04578.03442-.09677.03235-.14809-.00207-.05132-.01728-.10125-.04418-.14501-.02689-.04375-.06457-.07987-.10943-.10489-.04485-.02502-.09538-.03811-.14674-.03801H.299757c-.059058-.00005-.116788-.01752-.165955-.05024-.0491673-.03272-.087584-.07922-.1104352-.13368-.02285107-.05446-.02912021-.11445-.01802154-.17246.01109864-.058.03907164-.11144.08041244-.15362L8.09576.0913129c.232-.2349999.618.0230001.489.3280001l-2.297 5.414997c-.01945.04591-.02715.09594-.02241.14557.00475.04963.02179.0973.04958.13869.02779.04139.06546.07521.10961.09838.04414.02318.09336.03499.14322.03436l6.29104-.078c.0593-.00095.1176.01573.1675.04794.0499.03221.0891.0785.1127.133.0235.0545.0304.11477.0197.17317-.0108.0584-.0386.11231-.0799.15489l-.001.001Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 921 B

View File

@@ -1,4 +0,0 @@
<svg width="17" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m3.2916 7.53561-2.12 2.121C.421438 10.4068 0 11.4242 0 12.4851s.421438 2.0783 1.1716 2.8285c.75017.7502 1.76761 1.1716 2.8285 1.1716 1.0609 0 2.07834-.4214 2.8285-1.1716l2.828-2.828c.3715-.3714.6661-.8124.8671-1.2977.2011-.4853.3045-1.0055.3045-1.53079 0-.5253-.1034-1.04546-.3045-1.53078-.201-.48531-.4956-.92628-.8671-1.29772l-1.06 1.06c.23222.23216.41643.50778.54211.81114.12567.30336.19036.6285.19036.95686 0 .32836-.06469.65349-.19036.95689-.12568.3033-.30989.579-.54211.8111l-2.831 2.828c-.4715.4554-1.10301.7074-1.7585.7017-.65549-.0057-1.28252-.2686-1.74604-.7321-.46352-.4636-.72645-1.0906-.73214-1.7461-.0057-.6555.24629-1.287.70168-1.7585l2.12-2.12099-1.06-1.061h.001Z" fill="#000"/>
<path d="m12.1304 7.8886 2.121-2.12c.4655-.46947.7261-1.10423.7248-1.76538-.0014-.66114-.2646-1.29483-.732-1.7624-.4674-.46756-1.1011-.73093-1.7622-.73247-.6612-.00154-1.296.25887-1.7656.72425l-2.82899 2.828c-.23222.23216-.41643.50779-.5421.81114-.12568.30336-.19037.6285-.19037.95686 0 .32836.06469.65351.19037.95686.12567.30336.30988.57899.5421.81114l-1.06 1.06c-.37146-.37143-.66612-.8124-.86715-1.29772-.20103-.48531-.3045-1.00547-.3045-1.53078 0-.5253.10347-1.04546.3045-1.53078.20103-.48531.49569-.92628.86715-1.29772l2.828-2.828C10.4056.421438 11.423-1e-8 12.4839 0c1.0609 1e-8 2.0783.421438 2.8285 1.1716.7502.75017 1.1716 1.76761 1.1716 2.8285 0 1.0609-.4214 2.07834-1.1716 2.8285l-2.121 2.121-1.061-1.06v-.001Z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,4 +0,0 @@
<svg width="17" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m3.2916 7.53561-2.12 2.121C.421438 10.4068 0 11.4242 0 12.4851s.421438 2.0783 1.1716 2.8285c.75017.7502 1.76761 1.1716 2.8285 1.1716 1.0609 0 2.07834-.4214 2.8285-1.1716l2.828-2.828c.3715-.3714.6661-.8124.8671-1.2977.2011-.4853.3045-1.0055.3045-1.53079 0-.5253-.1034-1.04546-.3045-1.53078-.201-.48531-.4956-.92628-.8671-1.29772l-1.06 1.06c.23222.23216.41643.50778.54211.81114.12567.30336.19036.6285.19036.95686 0 .32836-.06469.65349-.19036.95689-.12568.3033-.30989.579-.54211.8111l-2.831 2.828c-.4715.4554-1.10301.7074-1.7585.7017-.65549-.0057-1.28252-.2686-1.74604-.7321-.46352-.4636-.72645-1.0906-.73214-1.7461-.0057-.6555.24629-1.287.70168-1.7585l2.12-2.12099-1.06-1.061h.001Z" fill="#fff"/>
<path d="m12.1304 7.8886 2.121-2.12c.4655-.46947.7261-1.10423.7248-1.76538-.0014-.66114-.2646-1.29483-.732-1.7624-.4674-.46756-1.1011-.73093-1.7622-.73247-.6612-.00154-1.296.25887-1.7656.72425l-2.82899 2.828c-.23222.23216-.41643.50779-.5421.81114-.12568.30336-.19037.6285-.19037.95686 0 .32836.06469.65351.19037.95686.12567.30336.30988.57899.5421.81114l-1.06 1.06c-.37146-.37143-.66612-.8124-.86715-1.29772-.20103-.48531-.3045-1.00547-.3045-1.53078 0-.5253.10347-1.04546.3045-1.53078.20103-.48531.49569-.92628.86715-1.29772l2.828-2.828C10.4056.421438 11.423-1e-8 12.4839 0c1.0609 1e-8 2.0783.421438 2.8285 1.1716.7502.75017 1.1716 1.76761 1.1716 2.8285 0 1.0609-.4214 2.07834-1.1716 2.8285l-2.121 2.121-1.061-1.06v-.001Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m9.9998 13.5998 5.9-5.9c.1833-.18333.4167-.275.7-.275.2833 0 .5167.09167.7.275.1833.18334.275.41667.275.7 0 .28334-.0917.51667-.275.7l-6.6 6.6c-.2.2-.4333.3-.7.3-.26666 0-.5-.1-.7-.3l-2.6-2.6c-.18333-.1833-.275-.4167-.275-.7 0-.2833.09167-.5167.275-.7.18334-.1833.41667-.275.7-.275.28334 0 .51667.0917.7.275l1.9 1.9Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 425 B

View File

@@ -1,3 +0,0 @@
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m14 14 20 20m-20 0 20-20" stroke="#FFF" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 200 B

View File

@@ -1,18 +0,0 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)">
<mask id="b" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M.667 8A7.333 7.333 0 0 1 8 .667c.368 0 .667.298.667.666v13.334a.667.667 0 0 1-.667.666A7.333 7.333 0 0 1 .667 8Zm6.666-5.963a6 6 0 0 0 0 11.926V2.037Z" fill="#fff"/>
<path d="M8 1.333a6.667 6.667 0 1 1 0 13.334V1.333Z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.333 1.333c0-.368.299-.666.667-.666a7.333 7.333 0 1 1 0 14.666.667.667 0 0 1-.667-.666V1.333Zm1.334.704v11.926a6 6 0 0 0 0-11.926Z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.333 4c0-.368.299-.667.667-.667h5a.667.667 0 0 1 0 1.334H3A.667.667 0 0 1 2.333 4ZM1 6.667C1 6.299 1.298 6 1.667 6H8a.667.667 0 0 1 0 1.333H1.667A.667.667 0 0 1 1 6.667Zm0 2.666c0-.368.298-.666.667-.666H8A.667.667 0 1 1 8 10H1.667A.667.667 0 0 1 1 9.333ZM2.333 12c0-.368.299-.667.667-.667h5a.667.667 0 0 1 0 1.334H3A.667.667 0 0 1 2.333 12Z" fill="#fff"/>
</mask>
<g mask="url(#b)">
<path d="M0 0h16v16H0V0Z" fill="#fff"/>
</g>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h16v16H0z"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" data-name="Layer 1" viewBox="0 0 24 24" width="512" height="512">
<path fill="white" d="M12,17a4,4,0,1,1,4-4A4,4,0,0,1,12,17Zm6,4a3,3,0,0,0-3-3H9a3,3,0,0,0-3,3v3H18ZM18,8a4,4,0,1,1,4-4A4,4,0,0,1,18,8ZM6,8a4,4,0,1,1,4-4A4,4,0,0,1,6,8Zm0,5A5.968,5.968,0,0,1,7.537,9H3a3,3,0,0,0-3,3v3H6.349A5.971,5.971,0,0,1,6,13Zm11.651,2H24V12a3,3,0,0,0-3-3H16.463a5.952,5.952,0,0,1,1.188,6Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 481 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 21H8V7h11m0-2H8c-.53043 0-1.03914.21071-1.41421.58579C6.21071 5.96086 6 6.46957 6 7v14c0 .5304.21071 1.0391.58579 1.4142C6.96086 22.7893 7.46957 23 8 23h11c.5304 0 1.0391-.2107 1.4142-.5858S21 21.5304 21 21V7c0-.53043-.2107-1.03914-.5858-1.41421C20.0391 5.21071 19.5304 5 19 5Zm-3-4H4c-.53043 0-1.03914.21071-1.41421.58579C2.21071 1.96086 2 2.46957 2 3v14h2V3h12V1Z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 478 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 21H8V7h11m0-2H8c-.53043 0-1.03914.21071-1.41421.58579C6.21071 5.96086 6 6.46957 6 7v14c0 .5304.21071 1.0391.58579 1.4142C6.96086 22.7893 7.46957 23 8 23h11c.5304 0 1.0391-.2107 1.4142-.5858S21 21.5304 21 21V7c0-.53043-.2107-1.03914-.5858-1.41421C20.0391 5.21071 19.5304 5 19 5Zm-3-4H4c-.53043 0-1.03914.21071-1.41421.58579C2.21071 1.96086 2 2.46957 2 3v14h2V3h12V1Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 478 B

View File

@@ -1,3 +0,0 @@
<svg width="15" height="12" viewBox="0 0 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 3.99992L4.16667 1.33325M4.16667 1.33325L6.83333 3.99992M4.16667 1.33325V10.6666M13.5 7.99992L10.8333 10.6666M10.8333 10.6666L8.16667 7.99992M10.8333 10.6666V1.33325" stroke="#A3A3A3" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 372 B

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icons">
<path id="Vector" d="M7 10L12 15L17 10" stroke="white" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 250 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 20C5.45 20 4.979 19.804 4.587 19.412C4.195 19.02 3.99934 18.5493 4 18V15H6V18H18V15H20V18C20 18.55 19.804 19.021 19.412 19.413C19.02 19.805 18.5493 20.0007 18 20H6ZM12 16L7 11L8.4 9.55L11 12.15V4H13V12.15L15.6 9.55L17 11L12 16Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 359 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5ZM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5Zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 318 B

View File

@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.667 1.66406H3.33366C2.41699 1.66406 1.67533 2.41406 1.67533 3.33073L1.66699 18.3307L5.00033 14.9974H16.667C17.5837 14.9974 18.3337 14.2474 18.3337 13.3307V3.33073C18.3337 2.41406 17.5837 1.66406 16.667 1.66406ZM5.83366 7.4974H14.167C14.6253 7.4974 15.0003 7.8724 15.0003 8.33073C15.0003 8.78906 14.6253 9.16406 14.167 9.16406H5.83366C5.37533 9.16406 5.00033 8.78906 5.00033 8.33073C5.00033 7.8724 5.37533 7.4974 5.83366 7.4974ZM10.8337 11.6641H5.83366C5.37533 11.6641 5.00033 11.2891 5.00033 10.8307C5.00033 10.3724 5.37533 9.9974 5.83366 9.9974H10.8337C11.292 9.9974 11.667 10.3724 11.667 10.8307C11.667 11.2891 11.292 11.6641 10.8337 11.6641ZM14.167 6.66406H5.83366C5.37533 6.66406 5.00033 6.28906 5.00033 5.83073C5.00033 5.3724 5.37533 4.9974 5.83366 4.9974H14.167C14.6253 4.9974 15.0003 5.3724 15.0003 5.83073C15.0003 6.28906 14.6253 6.66406 14.167 6.66406Z" fill="#B9B9B9"/>
</svg>

Before

Width:  |  Height:  |  Size: 996 B

View File

@@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icons">
<path id="Vector" d="M10 17L15 12L10 7" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 244 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.75 20.9531C3.75 21.368 4.08516 21.7031 4.5 21.7031H11.2031V12.8906H3.75V20.9531ZM12.7969 21.7031H19.5C19.9148 21.7031 20.25 21.368 20.25 20.9531V12.8906H12.7969V21.7031ZM20.625 7.26562H17.1656C17.4844 6.76406 17.6719 6.16875 17.6719 5.53125C17.6719 3.74766 16.2211 2.29688 14.4375 2.29688C13.4672 2.29688 12.593 2.72812 12 3.40781C11.407 2.72812 10.5328 2.29688 9.5625 2.29688C7.77891 2.29688 6.32812 3.74766 6.32812 5.53125C6.32812 6.16875 6.51328 6.76406 6.83438 7.26562H3.375C2.96016 7.26562 2.625 7.60078 2.625 8.01562V11.2969H11.2031V7.26562H12.7969V11.2969H21.375V8.01562C21.375 7.60078 21.0398 7.26562 20.625 7.26562ZM11.2031 7.17188H9.5625C8.65781 7.17188 7.92188 6.43594 7.92188 5.53125C7.92188 4.62656 8.65781 3.89062 9.5625 3.89062C10.4672 3.89062 11.2031 4.62656 11.2031 5.53125V7.17188ZM14.4375 7.17188H12.7969V5.53125C12.7969 4.62656 13.5328 3.89062 14.4375 3.89062C15.3422 3.89062 16.0781 4.62656 16.0781 5.53125C16.0781 6.43594 15.3422 7.17188 14.4375 7.17188Z" fill="#FA0050"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m9.9998 13.5998 5.9-5.9c.1833-.18333.4167-.275.7-.275.2833 0 .5167.09167.7.275.1833.18334.275.41667.275.7 0 .28334-.0917.51667-.275.7l-6.6 6.6c-.2.2-.4333.3-.7.3-.26666 0-.5-.1-.7-.3l-2.6-2.6c-.18333-.1833-.275-.4167-.275-.7 0-.2833.09167-.5167.275-.7.18334-.1833.41667-.275.7-.275.28334 0 .51667.0917.7.275l1.9 1.9Z" fill="hsla(163, 70%, 38%, 1)"/>
</svg>

Before

Width:  |  Height:  |  Size: 443 B

View File

@@ -1,5 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icons">
<path id="Vector" d="M10.0463 7.50016L9.44634 8.1135C8.96634 8.5935 8.66634 9.00016 8.66634 10.0002H7.33301V9.66683C7.33301 8.92683 7.63301 8.26016 8.11301 7.78016L8.93967 6.94016C9.18634 6.70016 9.33301 6.36683 9.33301 6.00016C9.33301 5.64654 9.19253 5.3074 8.94248 5.05735C8.69243 4.8073 8.3533 4.66683 7.99967 4.66683C7.64605 4.66683 7.30691 4.8073 7.05687 5.05735C6.80682 5.3074 6.66634 5.64654 6.66634 6.00016H5.33301C5.33301 5.29292 5.61396 4.61464 6.11406 4.11454C6.61415 3.61445 7.29243 3.3335 7.99967 3.3335C8.70692 3.3335 9.38519 3.61445 9.88529 4.11454C10.3854 4.61464 10.6663 5.29292 10.6663 6.00016C10.6654 6.56232 10.4426 7.10138 10.0463 7.50016ZM8.66634 12.6668H7.33301V11.3335H8.66634M7.99967 1.3335C7.1242 1.3335 6.25729 1.50593 5.44845 1.84097C4.63961 2.176 3.90469 2.66706 3.28563 3.28612C2.03539 4.53636 1.33301 6.23205 1.33301 8.00016C1.33301 9.76827 2.03539 11.464 3.28563 12.7142C3.90469 13.3333 4.63961 13.8243 5.44845 14.1594C6.25729 14.4944 7.1242 14.6668 7.99967 14.6668C9.76778 14.6668 11.4635 13.9644 12.7137 12.7142C13.964 11.464 14.6663 9.76827 14.6663 8.00016C14.6663 4.3135 11.6663 1.3335 7.99967 1.3335Z" fill="white"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 17h2v-6h-2v6Zm1-8c.2833 0 .521-.096.713-.288.192-.192.2877-.42933.287-.712 0-.28333-.096-.521-.288-.713-.192-.192-.4293-.28767-.712-.287-.2833 0-.521.096-.713.288-.192.192-.2877.42933-.287.712 0 .28333.096.521.288.713.192.192.4293.28767.712.287Zm0 13c-1.3833 0-2.68333-.2627-3.9-.788-1.21667-.5253-2.275-1.2377-3.175-2.137-.9-.9-1.61233-1.9583-2.137-3.175S2.00067 13.3833 2 12c0-1.3833.26267-2.68333.788-3.9.52533-1.21667 1.23767-2.275 2.137-3.175.9-.9 1.95833-1.61233 3.175-2.137S10.6167 2.00067 12 2c1.3833 0 2.6833.26267 3.9.788 1.2167.52533 2.275 1.23767 3.175 2.137.9.9 1.6127 1.95833 2.138 3.175.5253 1.21667.7877 2.5167.787 3.9 0 1.3833-.2627 2.6833-.788 3.9-.5253 1.2167-1.2377 2.275-2.137 3.175-.9.9-1.9583 1.6127-3.175 2.138-1.2167.5253-2.5167.7877-3.9.787Zm0-2c2.2333 0 4.125-.775 5.675-2.325C19.225 16.125 20 14.2333 20 12c0-2.23333-.775-4.125-2.325-5.675C16.125 4.775 14.2333 4 12 4c-2.23333 0-4.125.775-5.675 2.325C4.775 7.875 4 9.76667 4 12c0 2.2333.775 4.125 2.325 5.675C7.875 19.225 9.76667 20 12 20Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,3 +0,0 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 28.4H5.08333V29.7H8.95833V28.4H10.25V15.4H11.5417V12.8H10.25V11.5H7.66667V12.8H6.375V14.1H5.08333V15.4H8.95833V18H7.66667V19.3H5.08333V18H3.79167V12.8H5.08333V11.5H6.375V10.2H14.125V11.5H15.4167V12.8H16.7083V15.4H18V18H19.2917V19.3H20.5833V16.7H21.875V15.4H23.1667V12.8H24.4583V10.2H25.75V8.9H27.0417V6.3H29.625V5H32.2083V6.3H33.5V7.6H30.9167V6.3H29.625V7.6H28.3333V23.2H29.625V29.7H30.9167V31H25.75V29.7H24.4583V16.7H23.1667V18H21.875V20.6H20.5833V23.2H19.2917V25.8H16.7083V23.2H15.4167V20.6H14.125V16.7H12.8333V24.5H11.5417V28.4H10.25V29.7H8.95833V31H3.79167V29.7H2.5V28.4Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 709 B

View File

@@ -1,5 +0,0 @@
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.025 4.275A3.5 3.5 0 0 1 7.5 3.25h5.25a2 2 0 1 1 0 4H8V31h20V7.25h-4.75a2 2 0 1 1 0-4h5.25a3.5 3.5 0 0 1 3.5 3.5V31.5a3.5 3.5 0 0 1-3.5 3.5h-21A3.5 3.5 0 0 1 4 31.5V6.75a3.5 3.5 0 0 1 1.025-2.475Z" fill="#fff"/>
<path d="M12.75 3h10.5v4.5h-10.5V3Z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.75 3a2 2 0 0 1 2-2h10.5a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2h-10.5a2 2 0 0 1-2-2V3Zm4 2v.5h6.5V5h-6.5Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 569 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.707 19.7069 18 10.4139l-4.414-4.41402-9.293 9.29302c-.12794.1281-.21882.2884-.263.464L3 20.9999l5.242-1.03c.176-.044.337-.135.465-.263ZM21 7.41388c.3749-.37506.5856-.88367.5856-1.414s-.2107-1.03895-.5856-1.414l-1.586-1.586c-.3751-.37494-.8837-.58557-1.414-.58557-.5303 0-1.0389.21063-1.414.58557L15 4.58588l4.414 4.414L21 7.41388Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 442 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M9.8585 7.5L12.5002 10.1333V10C12.5002 9.33696 12.2368 8.70107 11.7679 8.23223C11.2991 7.76339 10.6632 7.5 10.0002 7.5H9.8585ZM6.27516 8.16667L7.56683 9.45833C7.52516 9.63333 7.50016 9.80833 7.50016 10C7.50016 10.663 7.76356 11.2989 8.2324 11.7678C8.70124 12.2366 9.33712 12.5 10.0002 12.5C10.1835 12.5 10.3668 12.475 10.5418 12.4333L11.8335 13.725C11.2752 14 10.6585 14.1667 10.0002 14.1667C8.89509 14.1667 7.83529 13.7277 7.05388 12.9463C6.27248 12.1649 5.8335 11.1051 5.8335 10C5.8335 9.34167 6.00016 8.725 6.27516 8.16667ZM1.66683 3.55833L3.56683 5.45833L3.94183 5.83333C2.56683 6.91667 1.4835 8.33333 0.833496 10C2.27516 13.6583 5.8335 16.25 10.0002 16.25C11.2918 16.25 12.5252 16 13.6502 15.55L14.0085 15.9L16.4418 18.3333L17.5002 17.275L2.72516 2.5M10.0002 5.83333C11.1052 5.83333 12.165 6.27232 12.9464 7.05372C13.7278 7.83512 14.1668 8.89493 14.1668 10C14.1668 10.5333 14.0585 11.05 13.8668 11.5167L16.3085 13.9583C17.5585 12.9167 18.5585 11.55 19.1668 10C17.7252 6.34167 14.1668 3.75 10.0002 3.75C8.8335 3.75 7.71683 3.95833 6.66683 4.33333L8.47516 6.125C8.95016 5.94167 9.4585 5.83333 10.0002 5.83333Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,3 +0,0 @@
<svg width="16" height="17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.333 13.008a.667.667 0 0 1-.666.667h-6A.667.667 0 0 1 3 13.008v-6a.667.667 0 1 1 1.333 0v4.39l7.348-7.346a.667.667 0 1 1 .942.942l-7.347 7.348h4.39c.369 0 .667.298.667.666Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 325 B

View File

@@ -1,3 +0,0 @@
<svg width="48" height="48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m14 14 20 20m-20 0 20-20" stroke="hsla(343, 92%, 54%, 1)" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 218 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17.6493 6.34989C16.8111 5.50416 15.7937 4.8575 14.6721 4.45768C13.5505 4.05786 12.3534 3.91508 11.1693 4.03989C7.49929 4.40989 4.47929 7.38989 4.06929 11.0599C3.51929 15.9099 7.26929 19.9999 11.9993 19.9999C13.5094 19.9999 14.9885 19.5714 16.2648 18.7642C17.5411 17.957 18.5621 16.8043 19.2093 15.4399C19.5293 14.7699 19.0493 13.9999 18.3093 13.9999C17.9393 13.9999 17.5893 14.1999 17.4293 14.5299C16.8487 15.7789 15.8557 16.7899 14.6172 17.3927C13.3788 17.9955 11.9705 18.1534 10.6293 17.8399C8.40929 17.3499 6.61929 15.5399 6.14929 13.3199C5.95172 12.4422 5.95401 11.5312 6.15598 10.6545C6.35796 9.77775 6.75445 8.95764 7.31614 8.25481C7.87783 7.55198 8.59033 6.98442 9.40096 6.59411C10.2116 6.20379 11.0996 6.00071 11.9993 5.99989C13.6593 5.99989 15.1393 6.68989 16.2193 7.77989L14.7093 9.28989C14.0793 9.91989 14.5193 10.9999 15.4093 10.9999H18.9993C19.5493 10.9999 19.9993 10.5499 19.9993 9.99989V6.40989C19.9993 5.51989 18.9193 5.06989 18.2893 5.69989L17.6493 6.34989Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.7469 12.7455C19.9345 12.558 20.0398 12.3036 20.0398 12.0384C20.0398 11.7732 19.9345 11.5188 19.7469 11.3313L14.1254 5.63909C13.9379 5.45156 13.6835 5.3462 13.4183 5.3462C13.1531 5.3462 12.8987 5.45156 12.7112 5.63909C12.5237 5.82663 12.4183 6.08098 12.4183 6.3462C12.4183 6.61142 12.5237 6.86577 12.7112 7.05331L16.6427 10.9848L4.93303 10.999C4.80102 10.9984 4.67021 11.024 4.54814 11.0743C4.42608 11.1246 4.31517 11.1985 4.22183 11.2918C4.12848 11.3852 4.05454 11.4961 4.00427 11.6182C3.954 11.7402 3.9284 11.871 3.92894 12.0031C3.9284 12.1351 3.954 12.2659 4.00427 12.3879C4.05454 12.51 4.12848 12.6209 4.22183 12.7143C4.31517 12.8076 4.42608 12.8815 4.54814 12.9318C4.67021 12.9821 4.80102 13.0077 4.93303 13.0071L16.6569 13.0071L12.7112 16.9528C12.5237 17.1403 12.4183 17.3947 12.4183 17.6599C12.4183 17.9251 12.5237 18.1795 12.7112 18.367C12.8987 18.5546 13.1531 18.6599 13.4183 18.6599C13.6835 18.6599 13.9379 18.5546 14.1254 18.367L19.7469 12.7455Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,16 +0,0 @@
<svg width="64" height="57" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="b" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="10" width="34" height="38">
<path fill="url(#a)" d="M30 10h34v38H30z"/>
</mask>
<g mask="url(#b)">
<path d="M49.294 46.586c8.25 0 13.219-4.008 13.219-10.313v-.023c0-5.273-3.07-8.133-10.102-9.586l-3.656-.75c-4.078-.844-5.93-2.25-5.93-4.64v-.024c0-2.695 2.461-4.547 6.422-4.57 3.797 0 6.399 1.758 6.797 4.71l.047.282h5.79l-.024-.399c-.352-5.789-5.18-9.68-12.563-9.68-7.289 0-12.515 4.032-12.539 9.985v.024c0 5.039 3.281 8.132 9.938 9.515l3.632.75c4.36.914 6.118 2.274 6.118 4.805v.023c0 2.907-2.672 4.805-6.938 4.805-4.242 0-7.219-1.805-7.664-4.71l-.047-.282h-5.789l.024.351c.398 6.07 5.507 9.727 13.265 9.727Z" fill="#F61D5B" fill-opacity=".5"/>
</g>
<path d="M2.977 46h6.046V33.344h6.54L22.218 46h6.89l-7.382-13.57c3.937-1.43 6.375-5.11 6.375-9.657v-.046c0-6.54-4.407-10.547-11.625-10.547h-13.5V46Zm6.046-17.438V17.078h6.704c3.796 0 6.187 2.156 6.187 5.695v.047c0 3.633-2.25 5.742-6.07 5.742h-6.82Z" fill="#F61D5B"/>
<path d="M44.86 46.586c8.25 0 13.218-4.008 13.218-10.313v-.023c0-5.273-3.07-8.133-10.101-9.586l-3.657-.75c-4.078-.844-5.93-2.25-5.93-4.64v-.024c0-2.695 2.462-4.547 6.422-4.57 3.797 0 6.399 1.758 6.797 4.71l.047.282h5.79l-.024-.399c-.352-5.789-5.18-9.68-12.563-9.68-7.289 0-12.515 4.032-12.539 9.985v.024c0 5.039 3.282 8.132 9.938 9.515l3.633.75c4.359.914 6.117 2.274 6.117 4.805v.023c0 2.907-2.672 4.805-6.938 4.805-4.242 0-7.218-1.805-7.664-4.71l-.047-.282H31.57l.024.351c.398 6.07 5.508 9.727 13.265 9.727Z" fill="#F61D5B" fill-opacity=".75"/>
<defs>
<linearGradient id="a" x1="64" y1="29.292" x2="30" y2="29.292" gradientUnits="userSpaceOnUse">
<stop stop-color="#black" stop-opacity=".5"/>
<stop offset="1" stop-color="#black"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 3c-.53043 0-1.03914.21071-1.41421.58579C3.21071 3.96086 3 4.46957 3 5v14c0 .5304.21071 1.0391.58579 1.4142C3.96086 20.7893 4.46957 21 5 21h14c.5304 0 1.0391-.2107 1.4142-.5858S21 19.5304 21 19V5.5L18.5 3H17v6c0 .26522-.1054.51957-.2929.70711C16.5196 9.89464 16.2652 10 16 10H8c-.26522 0-.51957-.10536-.70711-.29289C7.10536 9.51957 7 9.26522 7 9V3H5Zm7 1v5h3V4h-3Zm-5 8h10c.2652 0 .5196.1054.7071.2929S18 12.7348 18 13v6H6v-6c0-.2652.10536-.5196.29289-.7071C6.48043 12.1054 6.73478 12 7 12Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 601 B

View File

@@ -1,3 +0,0 @@
<svg width="37" height="36" viewBox="0 0 37 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M26 3H30.5C32.1569 3 33.5 4.34315 33.5 6V10.5H36.5V6C36.5 2.68629 33.8137 0 30.5 0H26V3ZM11 3V0H6.5C3.18629 0 0.5 2.68629 0.5 6V10.5H3.5V6C3.5 4.34315 4.84315 3 6.5 3H11ZM3.5 25.5H0.5V30C0.5 33.3137 3.18629 36 6.5 36H11V33H6.5C4.84315 33 3.5 31.6569 3.5 30V25.5ZM26 33V36H30.5C33.8137 36 36.5 33.3137 36.5 30V25.5H33.5V30C33.5 31.6569 32.1569 33 30.5 33H26Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 526 B

View File

@@ -1,3 +0,0 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.66667 4.16667C5.66667 3.79848 5.96515 3.5 6.33334 3.5H12.3333C12.7015 3.5 13 3.79848 13 4.16667V10.1667C13 10.5349 12.7015 10.8333 12.3333 10.8333C11.9651 10.8333 11.6667 10.5349 11.6667 10.1667V5.77614L4.31941 13.1234C4.05906 13.3838 3.63695 13.3838 3.3766 13.1234C3.11625 12.8631 3.11625 12.4409 3.3766 12.1806L10.7239 4.83333H6.33334C5.96515 4.83333 5.66667 4.53486 5.66667 4.16667Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 557 B

View File

@@ -1,3 +0,0 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M29.25 18C29.25 17.655 29.235 17.325 29.205 16.98L31.995 14.865C32.595 14.415 32.76 13.575 32.385 12.915L29.58 8.07C29.4001 7.75228 29.1092 7.51221 28.7631 7.39593C28.417 7.27965 28.0402 7.29534 27.705 7.44L24.48 8.805C23.925 8.415 23.34 8.07 22.725 7.785L22.29 4.32C22.2 3.57 21.555 3 20.805 3H15.21C14.445 3 13.8 3.57 13.71 4.32L13.275 7.785C12.66 8.07 12.075 8.415 11.52 8.805L8.29499 7.44C7.60499 7.14 6.79499 7.41 6.41999 8.07L3.61499 12.93C3.23999 13.59 3.40499 14.415 4.00499 14.88L6.79499 16.995C6.73281 17.6686 6.73281 18.3464 6.79499 19.02L4.00499 21.135C3.40499 21.585 3.23999 22.425 3.61499 23.085L6.41999 27.93C6.79499 28.59 7.60499 28.86 8.29499 28.56L11.52 27.195C12.075 27.585 12.66 27.93 13.275 28.215L13.71 31.68C13.8 32.43 14.445 33 15.195 33H20.79C21.54 33 22.185 32.43 22.275 31.68L22.71 28.215C23.325 27.93 23.91 27.585 24.465 27.195L27.69 28.56C28.38 28.86 29.19 28.59 29.565 27.93L32.37 23.085C32.745 22.425 32.58 21.6 31.98 21.135L29.19 19.02C29.235 18.675 29.25 18.345 29.25 18ZM18.06 23.25C15.165 23.25 12.81 20.895 12.81 18C12.81 15.105 15.165 12.75 18.06 12.75C20.955 12.75 23.31 15.105 23.31 18C23.31 20.895 20.955 23.25 18.06 23.25Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 23c-.55 0-1.021-.196-1.413-.588C4.195 22.02 3.99934 21.5493 4 21V10c0-.55.196-1.021.588-1.413C4.98 8.195 5.45067 7.99933 6 8h3v2H6v11h12V10h-3V8h3c.55 0 1.021.196 1.413.588.392.392.5877.86267.587 1.412v11c0 .55-.196 1.021-.588 1.413-.392.392-.8627.5877-1.412.587H6Zm5-7V4.825l-1.6 1.6L8 5l4-4 4 4-1.4 1.425-1.6-1.6V16h-2Z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 433 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 23c-.55 0-1.021-.196-1.413-.588C4.195 22.02 3.99934 21.5493 4 21V10c0-.55.196-1.021.588-1.413C4.98 8.195 5.45067 7.99933 6 8h3v2H6v11h12V10h-3V8h3c.55 0 1.021.196 1.413.588.392.392.5877.86267.587 1.412v11c0 .55-.196 1.021-.588 1.413-.392.392-.8627.5877-1.412.587H6Zm5-7V4.825l-1.6 1.6L8 5l4-4 4 4-1.4 1.425-1.6-1.6V16h-2Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 433 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 8.99985h3.5c.736 0 1.393.391 1.851 1.00095.32529-.60198.7255-1.16042 1.191-1.66195-.803-.823-1.866-1.339-3.042-1.339H4c-.26522 0-.51957.10536-.70711.29289C3.10536 7.48028 3 7.73463 3 7.99985s.10536.51957.29289.70711c.18754.18753.44189.29289.70711.29289Zm7.685 3.11095c.551-1.657 2.256-3.11095 3.649-3.11095h1.838l-1.293 1.29295c-.0928.0929-.1665.2031-.2167.3244-.0503.1213-.0761.2513-.0761.3826 0 .1314.0258.2614.0761.3827.0502.1213.1239.2315.2167.3243.0928.0929.2031.1665.3244.2168.1213.0502.2513.0761.3826.0761.1313 0 .2613-.0259.3826-.0761.1213-.0503.2316-.1239.3244-.2168L21 7.99985l-3.707-3.707c-.0928-.09285-.2031-.16649-.3244-.21674C16.8473 4.02586 16.7173 4 16.586 4c-.1313 0-.2613.02586-.3826.07611-.1213.05025-.2316.12389-.3244.21674-.0928.09284-.1665.20307-.2167.32437-.0503.12131-.0761.25133-.0761.38263 0 .1313.0258.26132.0761.38262.0502.12131.1239.23153.2167.32438l1.293 1.293h-1.838c-2.274 0-4.711 1.967-5.547 4.47895l-.472 1.411c-.641 1.926-2.072 3.11-2.815 3.11H4c-.26522 0-.51957.1054-.70711.2929-.18753.1876-.29289.4419-.29289.7071 0 .2653.10536.5196.29289.7072.18754.1875.44189.2928.70711.2928h2.5c1.837 0 3.863-1.925 4.713-4.479l.472-1.41Zm4.194 1.182c-.0929.0928-.1667.203-.217.3244-.0503.1213-.0762.2513-.0762.3826 0 .1314.0259.2614.0762.3827.0503.1214.1241.2316.217.3243l1.293 1.293h-2.338c-1.268 0-2.33-.891-2.691-2.108-.2661.773-.6326 1.5076-1.09 2.185.886 1.162 2.243 1.923 3.781 1.923h2.338l-1.293 1.293c-.0928.0929-.1665.2031-.2167.3244-.0503.1213-.0761.2513-.0761.3826 0 .1314.0258.2614.0761.3827.0502.1213.1239.2315.2167.3244.0928.0928.2031.1664.3244.2167.1213.0502.2513.0761.3826.0761.1313 0 .2613-.0259.3826-.0761.1213-.0503.2316-.1239.3244-.2167L21 16.9998l-3.707-3.707c-.0928-.0929-.203-.1666-.3243-.2169-.1213-.0504-.2514-.0763-.3827-.0763-.1313 0-.2614.0259-.3827.0763-.1213.0503-.2315.124-.3243.2169Z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 8.99985h3.5c.736 0 1.393.391 1.851 1.00095.32529-.60198.7255-1.16042 1.191-1.66195-.803-.823-1.866-1.339-3.042-1.339H4c-.26522 0-.51957.10536-.70711.29289C3.10536 7.48028 3 7.73463 3 7.99985s.10536.51957.29289.70711c.18754.18753.44189.29289.70711.29289Zm7.685 3.11095c.551-1.657 2.256-3.11095 3.649-3.11095h1.838l-1.293 1.29295c-.0928.0929-.1665.2031-.2167.3244-.0503.1213-.0761.2513-.0761.3826 0 .1314.0258.2614.0761.3827.0502.1213.1239.2315.2167.3243.0928.0929.2031.1665.3244.2168.1213.0502.2513.0761.3826.0761.1313 0 .2613-.0259.3826-.0761.1213-.0503.2316-.1239.3244-.2168L21 7.99985l-3.707-3.707c-.0928-.09285-.2031-.16649-.3244-.21674C16.8473 4.02586 16.7173 4 16.586 4c-.1313 0-.2613.02586-.3826.07611-.1213.05025-.2316.12389-.3244.21674-.0928.09284-.1665.20307-.2167.32437-.0503.12131-.0761.25133-.0761.38263 0 .1313.0258.26132.0761.38262.0502.12131.1239.23153.2167.32438l1.293 1.293h-1.838c-2.274 0-4.711 1.967-5.547 4.47895l-.472 1.411c-.641 1.926-2.072 3.11-2.815 3.11H4c-.26522 0-.51957.1054-.70711.2929-.18753.1876-.29289.4419-.29289.7071 0 .2653.10536.5196.29289.7072.18754.1875.44189.2928.70711.2928h2.5c1.837 0 3.863-1.925 4.713-4.479l.472-1.41Zm4.194 1.182c-.0929.0928-.1667.203-.217.3244-.0503.1213-.0762.2513-.0762.3826 0 .1314.0259.2614.0762.3827.0503.1214.1241.2316.217.3243l1.293 1.293h-2.338c-1.268 0-2.33-.891-2.691-2.108-.2661.773-.6326 1.5076-1.09 2.185.886 1.162 2.243 1.923 3.781 1.923h2.338l-1.293 1.293c-.0928.0929-.1665.2031-.2167.3244-.0503.1213-.0761.2513-.0761.3826 0 .1314.0258.2614.0761.3827.0502.1213.1239.2315.2167.3244.0928.0928.2031.1664.3244.2167.1213.0502.2513.0761.3826.0761.1313 0 .2613-.0259.3826-.0761.1213-.0503.2316-.1239.3244-.2167L21 16.9998l-3.707-3.707c-.0928-.0929-.203-.1666-.3243-.2169-.1213-.0504-.2514-.0763-.3827-.0763-.1313 0-.2614.0259-.3827.0763-.1213.0503-.2315.124-.3243.2169Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -1,5 +0,0 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icons">
<path id="Vector" d="M12.4105 1.91098C12.5668 1.75475 12.7787 1.66699 12.9997 1.66699C13.2206 1.66699 13.4326 1.75475 13.5888 1.91098L16.9222 5.24431C17.0784 5.40059 17.1662 5.61251 17.1662 5.83348C17.1662 6.05445 17.0784 6.26637 16.9222 6.42265L13.5888 9.75598C13.4317 9.90778 13.2212 9.99177 13.0027 9.98987C12.7842 9.98798 12.5752 9.90034 12.4207 9.74583C12.2662 9.59132 12.1785 9.38231 12.1766 9.16381C12.1747 8.94532 12.2587 8.73482 12.4105 8.57765L14.3213 6.66681H4.66634C4.44533 6.66681 4.23337 6.57902 4.07709 6.42274C3.92081 6.26646 3.83301 6.05449 3.83301 5.83348C3.83301 5.61247 3.92081 5.40051 4.07709 5.24423C4.23337 5.08794 4.44533 5.00015 4.66634 5.00015H14.3213L12.4105 3.08931C12.2543 2.93304 12.1665 2.72112 12.1665 2.50015C12.1665 2.27918 12.2543 2.06725 12.4105 1.91098ZM8.58884 10.2443C8.74507 10.4006 8.83283 10.6125 8.83283 10.8335C8.83283 11.0545 8.74507 11.2664 8.58884 11.4226L6.67801 13.3335H16.333C16.554 13.3335 16.766 13.4213 16.9223 13.5776C17.0785 13.7338 17.1663 13.9458 17.1663 14.1668C17.1663 14.3878 17.0785 14.5998 16.9223 14.7561C16.766 14.9123 16.554 15.0001 16.333 15.0001H6.67801L8.58884 16.911C8.74064 17.0681 8.82463 17.2786 8.82274 17.4971C8.82084 17.7156 8.7332 17.9247 8.57869 18.0792C8.42418 18.2337 8.21517 18.3213 7.99668 18.3232C7.77818 18.3251 7.56768 18.2411 7.41051 18.0893L4.07717 14.756C3.92095 14.5997 3.83319 14.3878 3.83319 14.1668C3.83319 13.9458 3.92095 13.7339 4.07717 13.5776L7.41051 10.2443C7.56678 10.0881 7.7787 10.0003 7.99967 10.0003C8.22064 10.0003 8.43257 10.0881 8.58884 10.2443Z" fill="#A3A3A3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.00002 3.33337v1.33334H10.39L2.66669 12.39l.94333.9434 7.72338-7.72336V10h1.3333V3.33337H6.00002Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 216 B

View File

@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5292 2.71983C10.3886 2.57938 10.1979 2.50049 9.99918 2.50049C9.80043 2.50049 9.6098 2.57938 9.46918 2.71983L5.21918 6.96983C5.0867 7.112 5.01457 7.30005 5.018 7.49435C5.02143 7.68865 5.10014 7.87404 5.23755 8.01145C5.37497 8.14886 5.56035 8.22758 5.75465 8.231C5.94896 8.23443 6.137 8.16231 6.27918 8.02983L9.99918 4.30983L13.7192 8.02983C13.7878 8.10351 13.8706 8.16262 13.9626 8.20361C14.0546 8.2446 14.154 8.26664 14.2547 8.26842C14.3554 8.2702 14.4554 8.25167 14.5488 8.21395C14.6422 8.17623 14.727 8.12009 14.7982 8.04887C14.8694 7.97765 14.9256 7.89281 14.9633 7.79943C15.001 7.70604 15.0195 7.60601 15.0178 7.50531C15.016 7.4046 14.994 7.30529 14.953 7.21329C14.912 7.12129 14.8529 7.03849 14.7792 6.96983L10.5292 2.71983ZM14.7792 13.0298L10.5292 17.2798C10.3886 17.4203 10.1979 17.4992 9.99918 17.4992C9.80043 17.4992 9.6098 17.4203 9.46918 17.2798L5.21918 13.0298C5.14549 12.9612 5.08639 12.8784 5.0454 12.7864C5.0044 12.6944 4.98236 12.5951 4.98059 12.4944C4.97881 12.3936 4.99733 12.2936 5.03505 12.2002C5.07278 12.1068 5.12892 12.022 5.20014 11.9508C5.27136 11.8796 5.35619 11.8234 5.44958 11.7857C5.54297 11.748 5.643 11.7295 5.7437 11.7312C5.8444 11.733 5.94372 11.7551 6.03571 11.796C6.12771 11.837 6.21051 11.8961 6.27918 11.9698L9.99918 15.6898L13.7192 11.9698C13.7878 11.8961 13.8706 11.837 13.9626 11.796C14.0546 11.7551 14.154 11.733 14.2547 11.7312C14.3554 11.7295 14.4554 11.748 14.5488 11.7857C14.6422 11.8234 14.727 11.8796 14.7982 11.9508C14.8694 12.022 14.9256 12.1068 14.9633 12.2002C15.001 12.2936 15.0195 12.3936 15.0178 12.4944C15.016 12.5951 14.994 12.6944 14.953 12.7864C14.912 12.8784 14.8529 12.9612 14.7792 13.0298Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 20C5.45 20 4.979 19.804 4.587 19.412C4.195 19.02 3.99934 18.5493 4 18V15H6V18H18V15H20V18C20 18.55 19.804 19.021 19.412 19.413C19.02 19.805 18.5493 20.0007 18 20H6ZM11 16V7.85L8.4 10.45L7 9L12 4L17 9L15.6 10.45L13 7.85V16H11Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 357 B

View File

@@ -1,3 +0,0 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 20c-.55 0-1.021-.196-1.413-.588C4.195 19.02 3.99934 18.5493 4 18v-3h2v3h12v-3h2v3c0 .55-.196 1.021-.588 1.413-.392.392-.8627.5877-1.412.587H6Zm5-4V7.85l-2.6 2.6L7 9l5-5 5 5-1.4 1.45-2.6-2.6V16h-2Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 308 B

View File

@@ -1,10 +0,0 @@
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#a)">
<path d="M15.945 21.15a10.498 10.498 0 1 1 19.11 8.7A10.47 10.47 0 0 1 25.5 36c-4.05 0-7.755-2.34-9.495-6H1.5v-3c.09-1.71 1.26-3.105 3.51-4.23 2.25-1.125 5.07-1.71 8.49-1.77.855 0 1.665.075 2.445.15ZM13.5 6c1.68.045 3.09.63 4.215 1.755s1.68 2.535 1.68 4.245-.555 3.12-1.68 4.245-2.535 1.68-4.215 1.68c-1.68 0-3.09-.555-4.215-1.68S7.605 13.71 7.605 12s.555-3.12 1.68-4.245S11.82 6.045 13.5 6Zm12 27a7.5 7.5 0 1 0 0-15 7.5 7.5 0 0 0 0 15ZM24 21h2.25v4.23l3.66 2.115-1.125 1.95L24 26.535V21Z" fill="#fff"/>
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h36v36H0z"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 725 B

View File

@@ -1,3 +0,0 @@
<svg width="36" height="36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6a6 6 0 1 1 0 12 6 6 0 0 1 0-12Zm0 15c6.63 0 12 2.685 12 6v3H6v-3c0-3.315 5.37-6 12-6Z" fill="#fff"/>
</svg>

Before

Width:  |  Height:  |  Size: 200 B

View File

@@ -1,18 +0,0 @@
export function Back() {
return (
<svg
width="36"
height="36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17.546 8 8 17.546l9.546 9.546"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}

View File

@@ -1,24 +0,0 @@
export function Paste() {
return (
<svg
width="36"
height="36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.025 4.275A3.5 3.5 0 0 1 7.5 3.25h5.25a2 2 0 1 1 0 4H8V31h20V7.25h-4.75a2 2 0 1 1 0-4h5.25a3.5 3.5 0 0 1 3.5 3.5V31.5a3.5 3.5 0 0 1-3.5 3.5h-21A3.5 3.5 0 0 1 4 31.5V6.75a3.5 3.5 0 0 1 1.025-2.475Z"
fill="currentColor"
/>
<path d="M12.75 3h10.5v4.5h-10.5V3Z" fill="currentColor" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.75 3a2 2 0 0 1 2-2h10.5a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2h-10.5a2 2 0 0 1-2-2V3Zm4 2v.5h6.5V5h-6.5Z"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -1,18 +0,0 @@
export function Scan() {
return (
<svg
width="37"
height="36"
viewBox="0 0 37 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M26 3H30.5C32.1569 3 33.5 4.34315 33.5 6V10.5H36.5V6C36.5 2.68629 33.8137 0 30.5 0H26V3ZM11 3V0H6.5C3.18629 0 0.5 2.68629 0.5 6V10.5H3.5V6C3.5 4.34315 4.84315 3 6.5 3H11ZM3.5 25.5H0.5V30C0.5 33.3137 3.18629 36 6.5 36H11V33H6.5C4.84315 33 3.5 31.6569 3.5 30V25.5ZM26 33V36H30.5C33.8137 36 36.5 33.3137 36.5 30V25.5H33.5V30C33.5 31.6569 32.1569 33 30.5 33H26Z"
fill="currentColor"
/>
</svg>
);
}

View File

@@ -1,27 +1,22 @@
import { TagItem } from "@mutinywallet/mutiny-wasm";
import { A } from "@solidjs/router";
import {
createEffect,
createResource,
createSignal,
For,
Match,
Show,
Switch
} from "solid-js";
import { cache, createAsync, revalidate, useNavigate } from "@solidjs/router";
import { Plus, Save, Search, Shuffle, Users } from "lucide-solid";
import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js";
import {
ActivityDetailsModal,
ActivityItem,
HackActivityType,
LoadingShimmer,
NiceP
} from "~/components";
import { ActivityDetailsModal, ButtonCard, NiceP } from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { createDeepSignal } from "~/utils";
import { timeAgo } from "~/utils";
interface IActivityItem {
import { GenericItem } from "./GenericItem";
export type HackActivityType =
| "Lightning"
| "OnChain"
| "ChannelOpen"
| "ChannelClose";
export interface IActivityItem {
kind: HackActivityType;
id: string;
amount_sats: number;
@@ -31,38 +26,139 @@ interface IActivityItem {
last_updated: number;
}
function UnifiedActivityItem(props: {
export function UnifiedActivityItem(props: {
item: IActivityItem;
onClick: (id: string, kind: HackActivityType) => void;
}) {
const navigate = useNavigate();
const click = () => {
props.onClick(
props.item.id,
props.item.kind as unknown as HackActivityType
);
};
const primaryContact = () => {
if (props.item.contacts.length === 0) {
return undefined;
}
return props.item.contacts[0];
};
// TODO: figure out what other shit we should filter out
const message = () => {
const filtered = props.item.labels.filter(
(l) => l !== "SWAP" && !l.startsWith("LN Channel:")
);
if (filtered.length === 0) {
return undefined;
}
return filtered[0];
};
const shouldShowShuffle = () => {
return (
props.item.kind === "ChannelOpen" ||
props.item.kind === "ChannelClose" ||
(props.item.labels.length > 0 && props.item.labels[0] === "SWAP")
);
};
const verb = () => {
if (props.item.kind === "ChannelOpen") {
return "opened a";
}
if (props.item.kind === "ChannelClose") {
return "closed a";
}
if (props.item.labels.length > 0 && props.item.labels[0] === "SWAP") {
return "swapped to";
}
if (
props.item.labels.length > 0 &&
props.item.labels[0] === "Swept Force Close"
) {
return undefined;
}
return "sent";
};
const primaryName = () => {
return props.item.inbound ? primaryContact()?.name || "Unknown" : "You";
};
const secondaryName = () => {
if (props.item.labels.length > 0 && props.item.labels[0] === "SWAP") {
return "Lightning";
}
if (
props.item.kind === "ChannelOpen" ||
props.item.kind === "ChannelClose"
) {
return "Lightning channel";
}
if (!props.item.inbound) {
return primaryContact()?.name || "Unknown";
}
return "you";
};
const shouldShowGeneric = () => {
if (props.item.inbound && primaryName() === "Unknown") {
return true;
}
if (!props.item.inbound && secondaryName() === "Unknown") {
return true;
}
};
return (
<ActivityItem
// This is actually the ActivityType enum but wasm is hard
kind={props.item.kind as unknown as HackActivityType}
labels={props.item.labels}
contacts={props.item.contacts}
// FIXME: is this something we can put into node logic?
amount={props.item.amount_sats || 0}
date={props.item.last_updated}
positive={props.item.inbound}
onClick={click}
/>
<div class="pt-3 first-of-type:pt-0">
<GenericItem
primaryAvatarUrl={primaryContact()?.image_url || ""}
icon={shouldShowShuffle() ? <Shuffle /> : undefined}
primaryOnClick={() =>
primaryName() !== "You" && primaryContact()?.id
? navigate(`/chat/${primaryContact()?.id}`)
: undefined
}
amountOnClick={click}
primaryName={
props.item.inbound
? primaryContact()?.name || "Unknown"
: "You"
}
genericAvatar={shouldShowGeneric()}
verb={verb()}
message={message()}
secondaryName={secondaryName()}
amount={
props.item.amount_sats
? BigInt(props.item.amount_sats || 0)
: undefined
}
date={timeAgo(props.item.last_updated)}
accent={props.item.inbound ? "green" : undefined}
visibility={
props.item.kind === "Lightning" ? "private" : undefined
}
/>
</div>
);
}
export function CombinedActivity(props: { limit?: number }) {
export function CombinedActivity() {
const [state, _actions] = useMegaStore();
const i18n = useI18n();
const [detailsOpen, setDetailsOpen] = createSignal(false);
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
const [detailsId, setDetailsId] = createSignal("");
const navigate = useNavigate();
function openDetailsModal(id: string, kind: HackActivityType) {
console.log("Opening details modal: ", id, kind);
@@ -77,26 +173,28 @@ export function CombinedActivity(props: { limit?: number }) {
setDetailsOpen(true);
}
async function fetchActivity() {
return await state.mutiny_wallet?.get_activity();
}
const getActivity = cache(async () => {
try {
console.log("refetching activity");
const activity = await state.mutiny_wallet?.get_activity();
return (activity || []) as IActivityItem[];
} catch (e) {
console.error(e);
return [] as IActivityItem[];
}
}, "activity");
const [activity, { refetch }] = createResource(fetchActivity, {
storage: createDeepSignal
});
const activity = createAsync(() => getActivity(), { initialValue: [] });
createEffect(() => {
// Should re-run after every sync
if (!state.is_syncing) {
refetch();
revalidate("activity");
}
});
return (
<Show
when={activity.state === "ready" || activity.state === "refreshing"}
fallback={<LoadingShimmer />}
>
<>
<Show when={detailsId() && detailsKind()}>
<ActivityDetailsModal
open={detailsOpen()}
@@ -106,47 +204,63 @@ export function CombinedActivity(props: { limit?: number }) {
/>
</Show>
<Switch>
<Match when={activity.latest.length === 0}>
<div class="w-full pb-4 text-center">
<NiceP>
{i18n.t(
"activity.receive_some_sats_to_get_started"
<Match when={activity().length === 0}>
<Show when={state.federations?.length === 0}>
<ButtonCard
onClick={() => navigate("/settings/federations")}
>
<div class="flex items-center gap-2">
<Users class="inline-block text-m-red" />
<NiceP>{i18n.t("home.federation")}</NiceP>
</div>
</ButtonCard>
</Show>
<ButtonCard onClick={() => navigate("/receive")}>
<div class="flex items-center gap-2">
<Plus class="inline-block text-m-red" />
<NiceP>{i18n.t("home.receive")}</NiceP>
</div>
</ButtonCard>
<ButtonCard onClick={() => navigate("/search")}>
<div class="flex items-center gap-2">
<Search class="inline-block text-m-red" />
<NiceP>{i18n.t("home.find")}</NiceP>
</div>
</ButtonCard>
<Show when={!state.has_backed_up}>
<ButtonCard
onClick={() => navigate("/settings/backup")}
>
<div class="flex items-center gap-2">
<Save class="inline-block text-m-red" />
<NiceP>{i18n.t("home.backup")}</NiceP>
</div>
</ButtonCard>
</Show>
</Match>
<Match when={activity().length >= 0}>
<Show when={!state.has_backed_up}>
<ButtonCard
onClick={() => navigate("/settings/backup")}
>
<div class="flex items-center gap-2">
<Save class="inline-block text-m-red" />
<NiceP>{i18n.t("home.backup")}</NiceP>
</div>
</ButtonCard>
</Show>
<div class="flex w-full flex-col divide-y divide-m-grey-800 overflow-x-clip">
<For each={activity()}>
{(activityItem) => (
<UnifiedActivityItem
item={activityItem}
onClick={openDetailsModal}
/>
)}
</NiceP>
</For>
</div>
</Match>
<Match
when={props.limit && activity.latest.length > props.limit}
>
<For each={activity.latest.slice(0, props.limit)}>
{(activityItem) => (
<UnifiedActivityItem
item={activityItem}
onClick={openDetailsModal}
/>
)}
</For>
</Match>
<Match when={activity.latest.length >= 0}>
<For each={activity.latest}>
{(activityItem) => (
<UnifiedActivityItem
item={activityItem}
onClick={openDetailsModal}
/>
)}
</For>
</Match>
</Switch>
{/* Only show on the home screen */}
<Show when={props.limit}>
<A
href="/activity"
class="self-center font-semibold text-m-red no-underline active:text-m-red/80"
>
{i18n.t("activity.view_all")}
</A>
</Show>
</Show>
</>
);
}

View File

@@ -4,22 +4,19 @@ import {
MutinyInvoice,
TagItem
} from "@mutinywallet/mutiny-wasm";
import { Copy, Link, Shuffle, Zap } from "lucide-solid";
import {
createEffect,
createMemo,
createResource,
Match,
ParentComponent,
Show,
Suspense,
Switch
} from "solid-js";
import bolt from "~/assets/icons/bolt.svg";
import chain from "~/assets/icons/chain.svg";
import copyIcon from "~/assets/icons/copy.svg";
import shuffle from "~/assets/icons/shuffle.svg";
import {
ActivityAmount,
AmountFiat,
AmountSats,
FancyCard,
@@ -59,6 +56,39 @@ interface OnChainTx {
labels: string[];
}
const ActivityAmount: ParentComponent<{
amount: string;
price: number;
positive?: boolean;
center?: boolean;
}> = (props) => {
return (
<div
class="flex flex-col gap-1"
classList={{
"items-end": !props.center,
"items-center": props.center
}}
>
<div
class="justify-end"
classList={{ "text-m-green": props.positive }}
>
<AmountSats
amountSats={Number(props.amount)}
icon={props.positive ? "plus" : undefined}
/>
</div>
<div class="text-sm text-white/70">
<AmountFiat
amountSats={Number(props.amount)}
denominationSize="sm"
/>
</div>
</div>
);
};
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
export const DIALOG_POSITIONER =
"fixed inset-0 z-50 flex items-center justify-center";
@@ -74,7 +104,7 @@ function LightningHeader(props: { info: MutinyInvoice }) {
{props.info.inbound
? i18n.t("activity.transaction_details.lightning_receive")
: i18n.t("activity.transaction_details.lightning_send")}
<img src={bolt} alt="lightning bolt" class="h-4 w-4" />
<Zap class="h-4 w-4" />
</div>
<div class="flex flex-col items-center">
<div
@@ -130,10 +160,10 @@ function OnchainHeader(props: { info: OnChainTx; kind?: HackActivityType }) {
props.kind === "ChannelClose"
}
>
<img src={shuffle} alt="swap" class="h-4 w-4" />
<Shuffle class="h-4 w-4" />
</Match>
<Match when={true}>
<img src={chain} alt="blockchain" class="h-4 w-4" />
<Link class="h-4 w-4" />
</Match>
</Switch>
</div>
@@ -172,7 +202,7 @@ export function MiniStringShower(props: { text: string }) {
classList={{ "bg-m-green rounded": copied() }}
onClick={() => copy(props.text)}
>
<img src={copyIcon} alt="copy" class="h-4 w-4" />
<Copy class="h-4 w-4" />
</button>
</div>
);
@@ -215,11 +245,13 @@ function LightningDetails(props: { info: MutinyInvoice; tags?: TagItem }) {
</TinyButton>
</KeyValue>
</Show>
<KeyValue key={i18n.t("activity.transaction_details.status")}>
{props.info.paid
? i18n.t("activity.transaction_details.paid")
: i18n.t("activity.transaction_details.unpaid")}
</KeyValue>
<Show when={!props.info.paid}>
<KeyValue
key={i18n.t("activity.transaction_details.status")}
>
i18n.t("activity.transaction_details.unpaid")
</KeyValue>
</Show>
<KeyValue key={i18n.t("activity.transaction_details.date")}>
<FormatPrettyPrint ts={Number(props.info.last_updated)} />
</KeyValue>
@@ -233,12 +265,7 @@ function LightningDetails(props: { info: MutinyInvoice; tags?: TagItem }) {
<KeyValue key={i18n.t("activity.transaction_details.invoice")}>
<MiniStringShower text={props.info.bolt11 ?? ""} />
</KeyValue>
<KeyValue
key={i18n.t("activity.transaction_details.payment_hash")}
>
<MiniStringShower text={props.info.payment_hash ?? ""} />
</KeyValue>
<Show when={props.info.paid}>
<Show when={props.info.paid && !props.info.inbound}>
<KeyValue
key={i18n.t(
"activity.transaction_details.payment_preimage"
@@ -369,11 +396,6 @@ function OnchainDetails(props: {
"Pending"
)}
</KeyValue>
<Show when={props.kind === "ChannelOpen" && channelInfo()}>
<KeyValue key={i18n.t("activity.transaction_details.peer")}>
<MiniStringShower text={channelInfo()?.peer ?? ""} />
</KeyValue>
</Show>
<KeyValue key={i18n.t("activity.transaction_details.txid")}>
<div class="flex gap-1">
{/* Have to do all these shenanigans because css / html is hard */}
@@ -412,7 +434,7 @@ function OnchainDetails(props: {
classList={{ "bg-m-green rounded": copied() }}
onClick={() => copy(props.info.txid)}
>
<img src={copyIcon} alt="copy" class="h-4 w-4" />
<Copy class="h-4 w-4" />
</button>
</div>
</KeyValue>

View File

@@ -1,172 +0,0 @@
import { TagItem } from "@mutinywallet/mutiny-wasm";
import { Match, ParentComponent, Switch } from "solid-js";
import bolt from "~/assets/icons/bolt.svg";
import chain from "~/assets/icons/chain.svg";
import shuffle from "~/assets/icons/shuffle.svg";
import { AmountFiat, AmountSats, LabelCircle } from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { timeAgo } from "~/utils";
export const ActivityAmount: ParentComponent<{
amount: string;
price: number;
positive?: boolean;
center?: boolean;
}> = (props) => {
return (
<div
class="flex flex-col gap-1"
classList={{
"items-end": !props.center,
"items-center": props.center
}}
>
<div
class="justify-end"
classList={{ "text-m-green": props.positive }}
>
<AmountSats
amountSats={Number(props.amount)}
icon={props.positive ? "plus" : undefined}
/>
</div>
<div class="text-sm text-white/70">
<AmountFiat
amountSats={Number(props.amount)}
denominationSize="sm"
/>
</div>
</div>
);
};
export type HackActivityType =
| "Lightning"
| "OnChain"
| "ChannelOpen"
| "ChannelClose";
export function ActivityItem(props: {
// This is actually the ActivityType enum but wasm is hard
kind: HackActivityType;
contacts: TagItem[];
labels: string[];
amount: number | bigint;
date?: number | bigint;
positive?: boolean;
onClick?: () => void;
}) {
const [state, _actions] = useMegaStore();
const i18n = useI18n();
const firstContact = () =>
props.contacts?.length ? props.contacts[0] : null;
// TODO: pass a value to the timeago function that will cause it to recalculate on sync
return (
<div
onClick={() => props.onClick && props.onClick()}
class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] gap-4 border-b border-neutral-800 pb-4 last:border-b-0"
classList={{ "cursor-pointer": !!props.onClick }}
>
<div class="flex items-center gap-2 md:gap-4">
<div class="">
<Switch>
<Match when={props.kind === "Lightning"}>
<img class="w-[1rem]" src={bolt} alt="lightning" />
</Match>
<Match when={props.kind === "OnChain"}>
<img class="w-[1rem]" src={chain} alt="onchain" />
</Match>
<Match
when={
props.kind === "ChannelOpen" ||
props.kind === "ChannelClose"
}
>
<img class="w-[1rem]" src={shuffle} alt="swap" />
</Match>
</Switch>
</div>
<div class="">
<LabelCircle
name={firstContact()?.name}
image_url={firstContact()?.image_url}
contact={props.contacts?.length > 0}
label={props.labels?.length > 0}
channel={props.kind}
/>
</div>
</div>
<div class="flex flex-col">
<Switch>
<Match when={props.kind === "ChannelClose"}>
<span class="text-base font-semibold text-neutral-500">
{i18n.t("activity.channel_close")}
</span>
</Match>
<Match when={props.kind === "ChannelOpen"}>
<span class="text-base font-semibold text-neutral-500">
{i18n.t("activity.channel_open")}
</span>{" "}
</Match>
<Match when={firstContact()?.name}>
<span class="truncate text-base font-semibold">
{firstContact()?.name}
</span>
</Match>
<Match when={props.labels.length > 0}>
<span class="truncate text-base font-semibold">
{props.labels[0]}
</span>
</Match>
<Match when={props.positive}>
<span class="text-base font-semibold text-neutral-500">
{i18n.t("activity.unknown")}
</span>
</Match>
<Match when={!props.positive}>
<span class="text-base font-semibold text-neutral-500">
{i18n.t("activity.unknown")}
</span>
</Match>
</Switch>
<Switch>
<Match when={props.date && props.date > 2147483647}>
<time class="text-sm text-m-yellow">
{i18n.t("common.pending")}
</time>
</Match>
<Match when={timeAgo(props.date) === "Pending"}>
<time class="text-sm text-m-yellow">
{i18n.t("common.pending")}
</time>
</Match>
<Match when={true}>
<time class="text-sm text-neutral-500">
{timeAgo(props.date)}
</time>
</Match>
</Switch>
</div>
<div class="">
<Switch>
<Match when={props.kind === "ChannelClose"}>
<div />
</Match>
<Match when={true}>
<ActivityAmount
amount={props.amount.toString()}
price={state.price}
positive={props.positive}
/>
</Match>{" "}
</Switch>
</div>
</div>
);
}

View File

@@ -1,8 +1,6 @@
import { Link, Users, Zap } from "lucide-solid";
import { Show } from "solid-js";
import bolt from "~/assets/icons/bolt.svg";
import chain from "~/assets/icons/chain.svg";
import community from "~/assets/icons/community.svg";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { satsToFormattedFiat } from "~/utils";
@@ -24,13 +22,13 @@ export function AmountSats(props: {
return (
<div class="flex items-center gap-2">
<Show when={props.icon === "lightning"}>
<img src={bolt} alt="lightning" class="h-[18px]" />
<Zap class="w-[18px]" />
</Show>
<Show when={props.icon === "community"}>
<img src={community} alt="community" class="h-[18px]" />
<Users class="w-[18px]" />
</Show>
<Show when={props.icon === "chain"}>
<img src={chain} alt="chain" class="h-[18px]" />
<Link class="w-[18px]" />
</Show>
<h1 class="whitespace-nowrap text-right font-light">
<Show when={props.icon === "plus"}>

View File

@@ -1,33 +1,41 @@
import { A, useNavigate } from "@solidjs/router";
import { A } from "@solidjs/router";
import { Shuffle } from "lucide-solid";
import { Match, Show, Switch } from "solid-js";
import shuffle from "~/assets/icons/shuffle.svg";
import {
AmountFiat,
AmountSats,
Button,
FancyCard,
Indicator,
InfoBox
InfoBox,
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
export function LoadingShimmer(props: { center?: boolean }) {
export function LoadingShimmer(props: { center?: boolean; small?: boolean }) {
return (
<div class="flex animate-pulse flex-col gap-2">
<h1
class="text-4xl font-light"
classList={{ "flex justify-center": props.center }}
>
<div class="h-[2.5rem] w-[12rem] rounded bg-neutral-700" />
<div
class="rounded bg-neutral-700"
classList={{
"h-[2.5rem] w-[12rem]": !props.small,
"h-[1rem] w-[8rem]": props.small
}}
/>
</h1>
<h2
class="text-xl font-light text-white/70"
classList={{ "flex justify-center": props.center }}
>
<div class="h-[1.75rem] w-[8rem] rounded bg-neutral-700" />
</h2>
<Show when={!props.small}>
<h2
class="text-xl font-light text-white/70"
classList={{ "flex justify-center": props.center }}
>
<div class="h-[1.75rem] w-[8rem] rounded bg-neutral-700" />
</h2>
</Show>
</div>
);
}
@@ -35,19 +43,10 @@ export function LoadingShimmer(props: { center?: boolean }) {
const STYLE =
"px-2 py-1 rounded-xl text-sm flex gap-2 items-center font-semibold";
export function BalanceBox(props: { loading?: boolean }) {
export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
const [state, _actions] = useMegaStore();
const i18n = useI18n();
const emptyBalance = () =>
(state.balance?.confirmed || 0n) === 0n &&
(state.balance?.lightning || 0n) === 0n &&
(state.balance?.federation || 0n) === 0n &&
(state.balance?.force_close || 0n) === 0n &&
(state.balance?.unconfirmed || 0n) === 0n;
const navigate = useNavigate();
const totalOnchain = () =>
(state.balance?.confirmed || 0n) +
(state.balance?.unconfirmed || 0n) +
@@ -57,8 +56,8 @@ export function BalanceBox(props: { loading?: boolean }) {
(state.balance?.confirmed || 0n) + (state.balance?.unconfirmed || 0n);
return (
<>
<FancyCard>
<VStack>
<FancyCard title="Lightning">
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<Switch>
<Match when={state.safe_mode}>
@@ -91,15 +90,16 @@ export function BalanceBox(props: { loading?: boolean }) {
</Match>
</Switch>
</Show>
<Show when={state.federations && state.federations.length}>
</FancyCard>
<Show when={state.federations && state.federations.length}>
<FancyCard title="Fedimint">
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<hr class="my-2 border-m-grey-750" />
<div class="flex justify-between">
<div class="flex flex-col gap-1">
<div class="text-2xl">
<AmountSats
amountSats={
state.balance?.federation || 0
state.balance?.federation || 0n
}
icon="community"
denominationSize="lg"
@@ -115,21 +115,20 @@ export function BalanceBox(props: { loading?: boolean }) {
/>
</div>
</div>
<Show when={state.balance?.federation || 0n > 0n}>
<div class="self-end justify-self-end">
<A href="/swaplightning" class={STYLE}>
<img
src={shuffle}
alt="swaplightning"
class="h-6 w-6"
/>
<Shuffle class="h-6 w-6" />
</A>
</div>
</Show>
</div>
</Show>
</Show>
<hr class="my-2 border-m-grey-750" />
</FancyCard>
</Show>
<FancyCard title="On-chain">
{/* <hr class="my-2 border-m-grey-750" /> */}
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<div class="flex justify-between">
<div class="flex flex-col gap-1">
@@ -159,11 +158,7 @@ export function BalanceBox(props: { loading?: boolean }) {
<Show when={usableOnchain() > 0n}>
<div class="self-end justify-self-end">
<A href="/swap" class={STYLE}>
<img
src={shuffle}
alt="swap"
class="h-6 w-6"
/>
<Shuffle class="h-6 w-6" />
</A>
</div>
</Show>
@@ -171,22 +166,6 @@ export function BalanceBox(props: { loading?: boolean }) {
</div>
</Show>
</FancyCard>
<div class="flex gap-2 py-4">
<Button
onClick={() => navigate("/search")}
disabled={emptyBalance() || props.loading}
intent="green"
>
{i18n.t("common.send")}
</Button>
<Button
onClick={() => navigate("/receive")}
disabled={props.loading}
intent="blue"
>
{i18n.t("common.receive")}
</Button>
</div>
</>
</VStack>
);
}

View File

@@ -1,6 +1,6 @@
import { ArrowDownUp } from "lucide-solid";
import { Show } from "solid-js";
import currencySwap from "~/assets/icons/currency-swap.svg";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { Currency } from "~/utils";
@@ -51,7 +51,7 @@ function SmallSubtleAmount(props: {
return (
<h2
class="flex flex-row items-end whitespace-nowrap text-xl font-light text-neutral-400"
class="flex flex-row items-center whitespace-nowrap text-xl font-light text-neutral-400"
tabIndex={0}
>
<Show when={!props.loading || props.mode === "fiat"} fallback="…">
@@ -65,13 +65,7 @@ function SmallSubtleAmount(props: {
<span class="text-base">
{props.fiat ? props.fiat.value : i18n.t("common.sats")}
</span>
<img
class={"pb-[4px] pl-[4px] hover:cursor-pointer"}
src={currencySwap}
height={24}
width={24}
alt="Swap currencies"
/>
<ArrowDownUp class="flex-0 inline-block h-6 w-6 pl-2 hover:cursor-pointer" />
</Show>
</h2>
);

View File

@@ -0,0 +1,28 @@
import { TagItem } from "@mutinywallet/mutiny-wasm";
import { LabelCircle } from "~/components";
import { PseudoContact } from "~/utils";
export function ContactButton(props: {
contact: PseudoContact | TagItem;
onClick: () => void;
}) {
return (
<button class="flex items-center gap-2" onClick={() => props.onClick()}>
<LabelCircle
name={props.contact.name}
image_url={props.contact.primal_image_url}
contact
label={false}
/>
<div class="flex flex-1 flex-col items-start">
<h2 class="overflow-hidden overflow-ellipsis text-base font-semibold">
{props.contact.name}
</h2>
<h3 class="overflow-hidden overflow-ellipsis text-left text-xs font-normal text-m-grey-400">
{props.contact.ln_address || ""}
</h3>
</div>
</button>
);
}

View File

@@ -1,7 +1,7 @@
import { SubmitHandler } from "@modular-forms/solid";
import { TagItem } from "@mutinywallet/mutiny-wasm";
import { useNavigate } from "@solidjs/router";
import { createSignal, Match, Show, Switch } from "solid-js";
import { createSignal, JSX, Match, Show, Switch } from "solid-js";
import {
Button,
@@ -11,7 +11,6 @@ import {
MiniStringShower,
showToast,
SimpleDialog,
SmallHeader,
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
@@ -25,8 +24,8 @@ export type ContactFormValues = {
};
export function ContactViewer(props: {
children: JSX.Element;
contact: TagItem;
gradient: string;
saveContact: (id: string, contact: ContactFormValues) => void;
deleteContact: (id: string) => Promise<void>;
}) {
@@ -80,25 +79,7 @@ export function ContactViewer(props: {
return (
<>
<button
onClick={() => setIsOpen(true)}
class="flex w-16 flex-shrink-0 flex-col items-center gap-2 overflow-x-hidden"
>
<div
class="flex h-16 w-16 flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 text-4xl uppercase"
style={{ background: props.gradient }}
>
<Switch>
<Match when={props.contact.image_url}>
<img src={props.contact.image_url} />
</Match>
<Match when={true}>{props.contact.name[0]}</Match>
</Switch>
</div>
<SmallHeader class="h-4 w-16 overflow-hidden overflow-ellipsis text-center">
{props.contact.name}
</SmallHeader>
</button>
<button onClick={() => setIsOpen(true)}>{props.children}</button>
<SimpleDialog
open={isOpen()}
setOpen={setIsOpen}
@@ -129,12 +110,7 @@ export function ContactViewer(props: {
<Match when={!isEditing()}>
<div class="mx-auto flex w-full max-w-[400px] flex-1 flex-col items-center justify-around gap-4">
<div class="flex w-full flex-col items-center">
<div
class="flex h-32 w-32 flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 text-8xl uppercase"
style={{
background: props.gradient
}}
>
<div class="flex h-32 w-32 flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 text-8xl uppercase">
<Switch>
<Match when={props.contact.image_url}>
<img

View File

@@ -20,6 +20,9 @@ export function DeleteEverything(props: { emergency?: boolean }) {
async function resetNode() {
try {
setConfirmLoading(true);
localStorage.removeItem("profile_setup_stage");
// If we're in a context where the wallet is loaded we want to use the regular action to delete it
// Otherwise we just call the import_json method directly
if (state.mutiny_wallet && !props.emergency) {

View File

@@ -0,0 +1,88 @@
import { createFileUploader } from "@solid-primitives/upload";
import { createSignal, Match, Switch } from "solid-js";
import { Button, SimpleInput } from "~/components";
import { useMegaStore } from "~/state/megaStore";
import { blobToBase64 } from "~/utils";
export type EditableProfile = {
nym?: string;
imageUrl?: string;
};
export function EditProfileForm(props: {
initialProfile?: EditableProfile;
onSave: (profile: EditableProfile) => Promise<void>;
saving: boolean;
cta: string;
}) {
const [state] = useMegaStore();
const [nym, setNym] = createSignal(props.initialProfile?.nym || "");
const [uploading, setUploading] = createSignal(false);
const { files, selectFiles } = createFileUploader({
multiple: false,
accept: "image/*"
});
async function uploadFile() {
selectFiles(async (files) => {
if (files.length) {
return;
}
});
}
async function onSave() {
try {
let imageUrl;
if (files() && files().length) {
setUploading(true);
const base64 = await blobToBase64(files()[0].file);
if (base64) {
imageUrl =
await state.mutiny_wallet?.upload_profile_pic(base64);
}
setUploading(false);
}
await props.onSave({
nym: nym(),
imageUrl: imageUrl ? imageUrl : props.initialProfile?.imageUrl
});
} catch (e) {
console.error(e);
}
}
return (
<>
<button
class="shiny-button flex h-[8rem] w-[8rem] flex-none items-center justify-center self-center overflow-clip rounded-full bg-m-grey-800 text-5xl uppercase"
onClick={uploadFile}
>
<Switch>
<Match when={files() && files().length}>
<img src={files()[0].source} />
</Match>
<Match when={props.initialProfile?.imageUrl}>
<img src={props.initialProfile?.imageUrl} />
</Match>
<Match when={true}>+</Match>
</Switch>
</button>
<SimpleInput
value={nym()}
onInput={(e) => setNym(e.currentTarget.value)}
placeholder="Your name or nym"
/>
<div class="flex-1" />
<Button
layout="full"
onClick={onSave}
loading={props.saving || uploading()}
>
{props.cta}
</Button>
</>
);
}

View File

@@ -8,7 +8,6 @@ import {
DefaultMain,
LargeHeader,
NiceP,
SafeArea,
SmallHeader
} from "~/components/layout";
import { useI18n } from "~/i18n/context";
@@ -28,37 +27,32 @@ export function ErrorDisplay(props: { error: Error }) {
console.error(props.error);
});
return (
<SafeArea>
<DefaultMain>
<Title>{i18n.t("error.general.oh_no")}</Title>
<DefaultMain>
<LargeHeader>{i18n.t("error.title")}</LargeHeader>
<SmallHeader>
{i18n.t("error.general.never_should_happen")}
</SmallHeader>
<SimpleErrorDisplay error={props.error} />
<NiceP>
{i18n.t("error.general.try_reloading")}{" "}
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
{i18n.t("error.general.support_link")}
</ExternalLink>
</NiceP>
<Button onClick={() => window.location.reload()}>
{i18n.t("error.reload")}
</Button>
<NiceP>
{i18n.t("error.general.getting_desperate")}{" "}
<A href="/settings/emergencykit">
{i18n.t("error.emergency_link")}
</A>
</NiceP>
<div class="h-full" />
<Button
onClick={() => (window.location.href = "/")}
intent="red"
>
{i18n.t("common.dangit")}
</Button>
</DefaultMain>
</SafeArea>
<LargeHeader>{i18n.t("error.title")}</LargeHeader>
<SmallHeader>
{i18n.t("error.general.never_should_happen")}
</SmallHeader>
<SimpleErrorDisplay error={props.error} />
<NiceP>
{i18n.t("error.general.try_reloading")}{" "}
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
{i18n.t("error.general.support_link")}
</ExternalLink>
</NiceP>
<Button onClick={() => window.location.reload()}>
{i18n.t("error.reload")}
</Button>
<NiceP>
{i18n.t("error.general.getting_desperate")}{" "}
<A href="/settings/emergencykit">
{i18n.t("error.emergency_link")}
</A>
</NiceP>
<div class="h-full" />
<Button onClick={() => (window.location.href = "/")} intent="red">
{i18n.t("common.dangit")}
</Button>
</DefaultMain>
);
}

171
src/components/Fab.tsx Normal file
View File

@@ -0,0 +1,171 @@
import { useNavigate } from "@solidjs/router";
import { ArrowDownLeft, ArrowUpRight, Plus, Scan } from "lucide-solid";
import { createSignal, JSX, onCleanup, onMount, Show } from "solid-js";
import { Circle } from "~/components";
import { useI18n } from "~/i18n/context";
function FabMenuItem(props: {
onClick: () => void;
disabled?: boolean;
children: JSX.Element;
}) {
return (
<button
class="flex gap-2 px-2 py-4 disabled:opacity-50"
disabled={props.disabled}
onClick={() => props.onClick()}
>
{props.children}
</button>
);
}
export function FabMenu(props: {
setOpen: (open: boolean) => void;
children: JSX.Element;
right?: boolean;
left?: boolean;
}) {
let navRef: HTMLElement;
const handleClickOutside = (e: MouseEvent) => {
if (e.target instanceof Element && !navRef.contains(e.target)) {
e.stopPropagation();
props.setOpen(false);
}
};
onMount(() => {
document.body.addEventListener("click", handleClickOutside);
});
onCleanup(() => {
document.body.removeEventListener("click", handleClickOutside);
});
return (
<nav
ref={(el) => (navRef = el)}
class="fixed z-50 rounded-xl bg-m-grey-800/90 px-2 backdrop-blur-lg"
classList={{
"right-8 bottom-[calc(2rem+5rem)]": props.right,
"left-2 bottom-[calc(2rem+2rem)]": props.left
}}
>
{props.children}
</nav>
);
}
export function Fab(props: { onSearch: () => void; onScan: () => void }) {
const [open, setOpen] = createSignal(false);
const navigate = useNavigate();
const i18n = useI18n();
return (
<>
<Show when={open()}>
<FabMenu setOpen={setOpen} right>
<ul class="flex flex-col divide-y divide-m-grey-400/25">
<li>
<FabMenuItem
onClick={() => {
props.onSearch();
setOpen(false);
}}
>
<ArrowUpRight />
{i18n.t("common.send")}
</FabMenuItem>
</li>
<li>
<FabMenuItem onClick={() => navigate("/receive")}>
<ArrowDownLeft />
{i18n.t("common.receive")}
</FabMenuItem>
</li>
<li>
<FabMenuItem
onClick={() => {
setOpen(false);
props.onScan();
}}
>
<Scan />
{i18n.t("common.scan")}
</FabMenuItem>
</li>
</ul>
</FabMenu>
</Show>
<div class="fixed bottom-8 right-8 text-m-red">
<button id="fab" onClick={() => setOpen(!open())}>
<Circle size="large">
<Plus class="h-8 w-8" />
</Circle>
</button>
</div>
</>
);
}
export function MiniFab(props: {
onSend: () => void;
onRequest: () => void;
onScan: () => void;
sendDisabled?: boolean | undefined;
}) {
const [open, setOpen] = createSignal(false);
const i18n = useI18n();
return (
<>
<Show when={open()}>
<FabMenu setOpen={setOpen} left>
<ul class="flex flex-col divide-y divide-m-grey-400/25">
<li>
<FabMenuItem
disabled={props.sendDisabled || false}
onClick={() => {
props.onSend();
setOpen(false);
}}
>
<ArrowUpRight />
{i18n.t("common.send")}
</FabMenuItem>
</li>
<li>
<FabMenuItem
onClick={() => {
props.onRequest();
setOpen(false);
}}
>
<ArrowDownLeft />
{i18n.t("common.request")}
</FabMenuItem>
</li>
<li>
<FabMenuItem
onClick={() => {
props.onScan();
setOpen(false);
}}
>
<Scan />
{i18n.t("common.scan")}
</FabMenuItem>
</li>
</ul>
</FabMenu>
</Show>
<button id="fab" onClick={() => setOpen(true)}>
<Plus class="h-8 w-8 text-m-red" />
</button>
</>
);
}

View File

@@ -20,7 +20,7 @@ export function Fee(props: { amountSats?: bigint | number }) {
</div>
</div>
<div>
<FeesModal icon />
<FeesModal />
</div>
</div>
</div>

View File

@@ -0,0 +1,193 @@
import { Check, Clock4, EyeOff, Globe, X, Zap } from "lucide-solid";
import { JSX, Match, Show, Switch } from "solid-js";
import { LabelCircle, LoadingSpinner } from "~/components";
export function GenericItem(props: {
primaryAvatarUrl?: string;
icon?: JSX.Element;
secondaryAvatarUrl?: string;
primaryName: string;
secondaryName?: string;
verb?: string;
amount?: bigint;
date?: string;
due?: string;
message?: string;
accent?: "green";
visibility?: "public" | "private";
showFiat?: boolean;
genericAvatar?: boolean;
forceSecondary?: boolean;
link?: string;
primaryOnClick?: () => void;
amountOnClick?: () => void;
secondaryOnClick?: () => void;
approveAction?: () => void;
rejectAction?: () => void;
shouldSpinny?: boolean;
}) {
return (
<div
class="grid w-full py-3 first-of-type:pt-0"
classList={{
"grid-cols-[auto_1fr_auto]": true,
"opacity-50": props.shouldSpinny
}}
>
<div class="self-center">
<Switch>
<Match when={props.icon}>
<button
class="flex h-[3rem] w-[3rem] items-center justify-center"
onClick={() => props.primaryOnClick}
>
{props.icon}
</button>
</Match>
<Match when={true}>
<LabelCircle
label={false}
name={props.primaryName}
contact
image_url={props.primaryAvatarUrl}
generic={props.genericAvatar}
onClick={props.primaryOnClick}
/>
</Match>
</Switch>
</div>
<div class="flex flex-col items-start justify-center gap-1 self-center px-2">
{/* TITLE TEXT */}
<Show when={props.primaryName && props.verb}>
<h2 class="text-sm">
<strong
classList={{
"text-m-grey-400": props.genericAvatar
}}
>
{props.primaryName}
</strong>
<span class="font-light">{` ${props.verb} `}</span>
<Show when={props.secondaryName}>
<strong>{props.secondaryName}</strong>
</Show>
</h2>
</Show>
<div class="flex flex-wrap gap-1">
{/* AMOUNT */}
<Show when={props.amount}>
<button
onClick={() =>
props.amountOnClick && props.amountOnClick()
}
class="flex items-center gap-1 rounded-full px-2 py-1 text-xs font-semibold text-white"
classList={{
"bg-m-grey-800": !props.accent,
"bg-m-green/40 ": props.accent === "green"
}}
>
{/* <img src={bolt} width={8} height={8} /> */}
<Zap class="w-3" fill="currentColor" />
{`${props.amount!.toLocaleString()} sats`}
</button>
</Show>
{/* FIAT AMOUNT */}
<Show when={props.showFiat}>
<div class="flex items-center gap-1 rounded-full py-1 text-xs font-semibold text-m-grey-400">
{`~$42.00 USD`}
</div>
</Show>
{/* OPTIONAL MESSAGE */}
<Show when={props.message}>
<div class="font-regular line-clamp-1 min-w-0 break-all rounded-full bg-m-grey-800 px-2 py-1 text-xs leading-6">
{props.message}
</div>
</Show>
{/* DUE */}
<Show when={props.due}>
<div class="flex w-full items-center gap-1 text-m-grey-400">
<Clock4 class="w-3" />
<span class="text-xs text-m-grey-400">
{props.due}
</span>
</div>
</Show>
</div>
{/* DATE WITH SECOND AVATAR */}
<Show when={props.date}>
<div class="flex items-center gap-1 text-m-grey-400">
<Show when={props.visibility === "public"}>
{/* <img src={globe} width={12} height={12} /> */}
<Globe class="w-3" />
</Show>
<Show when={props.visibility === "private"}>
<EyeOff class="w-3" />
{/* <img src={privateEye} width={12} height={12} /> */}
</Show>
<Show when={props.link && props.date}>
<a
href={props.link}
class="text-xs text-m-grey-400"
target="_blank"
rel="noopener noreferrer"
>
{props.date}
</a>
</Show>
<Show when={!props.link && props.date}>
<span class="text-xs text-m-grey-400">
{props.date}
</span>
</Show>
</div>
</Show>
</div>
<Show when={props.secondaryAvatarUrl || props.forceSecondary}>
<div class="self-center">
<LabelCircle
label={false}
name={props.secondaryName}
contact
image_url={props.secondaryAvatarUrl}
onClick={props.secondaryOnClick}
/>
</div>
</Show>
<Show when={!props.secondaryAvatarUrl && !props.forceSecondary}>
<div class="self-center">
{/* ACTIONS */}
<Show
when={
props.approveAction &&
props.rejectAction &&
!props.shouldSpinny
}
>
<div class="flex gap-4">
<button
class="flex h-10 w-10 items-center justify-center rounded bg-m-grey-800 p-1 text-m-green active:-mb-[1px] active:mt-[1px]"
onClick={() =>
props.approveAction && props.approveAction()
}
>
<Check />
</button>
<button
class="flex h-10 w-10 items-center justify-center rounded bg-m-grey-800 p-1 text-m-red active:-mb-[1px] active:mt-[1px]"
onClick={() =>
props.rejectAction && props.rejectAction()
}
>
<X />
</button>
</div>
</Show>
<Show when={props.shouldSpinny}>
<LoadingSpinner wide small />
</Show>
</div>
</Show>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import { A, useLocation } from "@solidjs/router";
import { Gift } from "lucide-solid";
import gift from "~/assets/icons/gift.svg";
import { useI18n } from "~/i18n/context";
export function GiftLink() {
@@ -16,7 +16,7 @@ export function GiftLink() {
}}
>
{i18n.t("settings.gift.give_sats_link")}
<img src={gift} class="h-5 w-5" alt="Gift" />
<Gift class="h-5 w-5" />
</A>
);
}

View File

@@ -0,0 +1,54 @@
import { Match, Switch } from "solid-js";
import { AmountFiat, AmountSats } from "~/components/Amount";
import { useMegaStore } from "~/state/megaStore";
export function HomeBalance() {
const [state, actions] = useMegaStore();
const lightningPlusFedi = () =>
(state.balance?.federation || 0n) + (state.balance?.lightning || 0n);
// TODO: do some sort of status indicator
// const fullyReady = () => state.load_stage === "done" && state.price !== 0;
return (
<button
onClick={actions.cycleBalanceView}
class="flex h-12 items-center justify-center rounded-lg border-b border-t border-b-white/10 border-t-white/40 bg-black px-4 py-2"
>
{/* <div class="w-2">
<div
title={fullyReady() ? "READY" : "ALMOST"}
class="h-2 w-2 animate-throb rounded-full border-2"
classList={{
"border-m-green bg-m-green": fullyReady(),
"border-m-yellow bg-m-yellow": !fullyReady()
}}
/>
</div> */}
<h1 class="flex w-full justify-center whitespace-nowrap text-2xl font-light text-white">
<Switch>
<Match when={state.balanceView === "sats"}>
<AmountSats
amountSats={lightningPlusFedi()}
icon="lightning"
denominationSize="lg"
/>
</Match>
<Match when={state.balanceView === "fiat"}>
<AmountFiat
amountSats={lightningPlusFedi()}
denominationSize="lg"
/>
</Match>
<Match when={state.balanceView === "hidden"}>
<div class="flex items-center gap-2">
<span>*****</span>
</div>
</Match>
</Switch>
</h1>
</button>
);
}

View File

@@ -0,0 +1,128 @@
import { useSearchParams } from "@solidjs/router";
import {
createEffect,
createResource,
createSignal,
Show,
Suspense
} from "solid-js";
import {
CombinedActivity,
LoadingShimmer,
NostrActivity,
PendingNwc,
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
export function HomeSubnav() {
const [state, _actions] = useMegaStore();
const i18n = useI18n();
const [params] = useSearchParams<{
tab: "me" | "everybody" | "requests";
}>();
const [activeView, setActiveView] = createSignal<
"me" | "everybody" | "requests"
>(params.tab || "me");
const [pending, { refetch }] = createResource(async () => {
try {
const pending =
await state.mutiny_wallet?.get_pending_nwc_invoices();
return pending?.length || 0;
} catch (e) {
console.error(e);
return 0;
}
});
createEffect(() => {
if (state.is_syncing) {
refetch();
}
});
return (
<>
<div class="flex gap-2">
<button
class="rounded px-2 py-1 text-sm"
classList={{
"bg-m-red": activeView() === "me",
"bg-m-grey-800 text-m-grey-400": activeView() !== "me"
}}
onClick={() => setActiveView("me")}
>
{i18n.t("home.subnav.just_me")}
</button>
<button
class="rounded px-2 py-1 text-sm"
classList={{
"bg-m-red": activeView() === "everybody",
"bg-m-grey-800 text-m-grey-400":
activeView() !== "everybody"
}}
onClick={() => setActiveView("everybody")}
>
{i18n.t("home.subnav.friends")}
</button>
<button
class="flex items-center gap-1 rounded px-2 py-1 text-sm"
classList={{
"bg-m-red": activeView() === "requests",
"bg-m-grey-800 text-m-grey-400":
activeView() !== "requests"
}}
onClick={() => setActiveView("requests")}
>
<span>{i18n.t("home.subnav.requests")}</span>
<Suspense fallback={<></>}>
<Show when={pending.latest && pending.latest > 0}>
<span
class="inline-flex h-5 w-5 items-center justify-center rounded-full bg-white/20 text-xs"
classList={{
"text-white": !!((pending.latest || 0) > 0)
}}
>
{pending()}
</span>
</Show>
</Suspense>
</button>
</div>
<Show when={activeView() === "me"}>
<VStack>
<Suspense>
<Show
when={!state.wallet_loading}
fallback={<LoadingShimmer />}
>
<CombinedActivity />
</Show>
</Suspense>
</VStack>
</Show>
<Show when={activeView() === "everybody"}>
<Suspense fallback={<LoadingShimmer />}>
<NostrActivity />
</Suspense>
</Show>
<Show when={activeView() === "requests"}>
<Suspense fallback={<LoadingShimmer />}>
<Show when={!state.wallet_loading && !state.safe_mode}>
<PendingNwc />
</Show>
</Suspense>
</Show>
{/* spacer just so we can always scroll above the fab */}
<div class="h-[4rem]" />
</>
);
}

View File

@@ -1,6 +1,6 @@
import { X } from "lucide-solid";
import { Show } from "solid-js";
import close from "~/assets/icons/black-close.svg";
import { ButtonLink } from "~/components";
import { useMegaStore } from "~/state/megaStore";
@@ -40,7 +40,7 @@ export function IOSbanner() {
onClick={closeBanner}
class="self-center justify-self-center rounded-lg hover:bg-white/10 active:bg-m-blue"
>
<img src={close} alt="Close" class="h-8 w-8" />
<X class="h-8 w-8 text-black" />
</button>{" "}
</div>
</div>

View File

@@ -1,7 +1,6 @@
import { Info } from "lucide-solid";
import { ParentComponent } from "solid-js";
import info from "~/assets/icons/info.svg";
export const InfoBox: ParentComponent<{
accent: "red" | "blue" | "green" | "white";
}> = (props) => {
@@ -16,7 +15,7 @@ export const InfoBox: ParentComponent<{
}}
>
<div class="self-center">
<img src={info} alt="info" class="h-8 w-8" />
<Info class="h-8 w-8" />
</div>
<div class="flex items-center">
<p class="text-sm font-light">{props.children}</p>

View File

@@ -1,16 +1,13 @@
import { Copy, Link, Share, Zap } from "lucide-solid";
import { Match, Show, Switch } from "solid-js";
import { QRCodeSVG } from "solid-qr-code";
import boltBlack from "~/assets/icons/bolt-black.svg";
import chainBlack from "~/assets/icons/chain-black.svg";
import copyBlack from "~/assets/icons/copy-black.svg";
import shareBlack from "~/assets/icons/share-black.svg";
import { AmountFiat, AmountSats, TruncateMiddle } from "~/components";
import { useI18n } from "~/i18n/context";
import { ReceiveFlavor } from "~/routes/Receive";
import { useCopy } from "~/utils";
function KindIndicator(props: { kind: ReceiveFlavor | "gift" }) {
function KindIndicator(props: { kind: ReceiveFlavor | "gift" | "lnAddress" }) {
const i18n = useI18n();
return (
<div class="flex flex-col items-end text-black">
@@ -19,21 +16,28 @@ function KindIndicator(props: { kind: ReceiveFlavor | "gift" }) {
<h3 class="font-semibold">
{i18n.t("receive.integrated_qr.onchain")}
</h3>
<img src={chainBlack} alt="chain" />
<Link class="h-4 w-4" />
</Match>
<Match when={props.kind === "lightning"}>
<h3 class="font-semibold">
{i18n.t("receive.integrated_qr.lightning")}
</h3>
<img src={boltBlack} alt="bolt" />
<Zap class="h-4 w-4" />
</Match>
<Match when={props.kind === "lnAddress"}>
<h3 class="font-semibold">
{i18n.t("contacts.ln_address")}
</h3>
<Zap class="h-4 w-4" />
</Match>
<Match when={props.kind === "gift"}>
<h3 class="font-semibold">
{i18n.t("receive.integrated_qr.gift")}
</h3>
<img src={boltBlack} alt="bolt" />
<Zap class="h-4 w-4" />
</Match>
<Match when={props.kind === "unified"}>
@@ -41,8 +45,8 @@ function KindIndicator(props: { kind: ReceiveFlavor | "gift" }) {
{i18n.t("receive.integrated_qr.unified")}
</h3>
<div class="flex gap-1">
<img src={chainBlack} alt="chain" />
<img src={boltBlack} alt="bolt" />
<Zap class="h-4 w-4" />
<Link class="h-4 w-4" />
</div>
</Match>
</Switch>
@@ -68,41 +72,46 @@ async function share(receiveString: string) {
export function IntegratedQr(props: {
value: string;
amountSats: string;
kind: ReceiveFlavor | "gift";
kind: ReceiveFlavor | "gift" | "lnAddress";
amountSats?: string;
}) {
const i18n = useI18n();
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
return (
<div
id="qr"
class="relative flex w-full flex-col items-center rounded-xl bg-white px-4"
class="relative flex w-full flex-col items-center rounded-xl bg-white px-4 text-black"
onClick={() => copy(props.value)}
>
<Show when={copied()}>
<div class="absolute z-50 flex h-full w-full flex-col items-center justify-center rounded-xl bg-neutral-900/60 transition-all">
<div class="absolute z-50 flex h-full w-full flex-col items-center justify-center rounded-xl bg-neutral-900/60 text-white transition-all">
<p class="text-xl font-bold">{i18n.t("common.copied")}</p>
</div>
</Show>
<div
class="flex w-full max-w-[256px] items-center py-4"
classList={{
"justify-between": props.kind !== "onchain",
"justify-end": props.kind === "onchain"
}}
>
<Show when={props.kind !== "onchain"}>
<div class="flex flex-col gap-1">
<div class="text-black">
<Show when={props.kind !== "lnAddress"}>
<div
class="flex w-full max-w-[256px] items-center py-4"
classList={{
"justify-between": props.kind !== "onchain",
"justify-end": props.kind === "onchain"
}}
>
<Show when={props.kind !== "onchain"}>
<div class="flex flex-col gap-1">
<AmountSats amountSats={Number(props.amountSats)} />
<div class="text-sm ">
<AmountFiat
amountSats={Number(props.amountSats)}
/>
</div>
</div>
<div class="text-sm text-black">
<AmountFiat amountSats={Number(props.amountSats)} />
</div>
</div>
</Show>
<KindIndicator kind={props.kind} />
</div>
</Show>
<KindIndicator kind={props.kind} />
</div>
</Show>
<Show when={props.kind === "lnAddress"}>
<div class="py-4" />
</Show>
<QRCodeSVG
value={props.value}
@@ -120,18 +129,20 @@ export function IntegratedQr(props: {
class="justify-self-start"
onClick={(_) => share(props.value)}
>
<img src={shareBlack} alt="share" />
<Share />
</button>
</Show>
<Show when={props.kind !== "lnAddress"}>
<div class="">
<TruncateMiddle text={props.value} whiteBg />
</div>
<button
class=" justify-self-end"
onClick={() => copy(props.value)}
>
<Copy />
</button>
</Show>
<div class="">
<TruncateMiddle text={props.value} whiteBg />
</div>
<button
class=" justify-self-end"
onClick={() => copy(props.value)}
>
<img src={copyBlack} alt="copy" />
</button>
</div>
</div>
);

View File

@@ -1,16 +1,47 @@
import { createResource, createSignal, Match, Switch } from "solid-js";
import { createResource, createSignal, JSX, Match, Switch } from "solid-js";
import { Dynamic } from "solid-js/web";
import off from "~/assets/icons/download-channel.svg";
import on from "~/assets/icons/upload-channel.svg";
import { HackActivityType } from "~/components";
import avatar from "~/assets/generic-avatar.jpg";
import { generateGradient } from "~/utils";
export function Circle(props: {
children: JSX.Element;
color?: "red" | "green" | "blue";
size?: "small" | "large" | "xl";
background?: string;
onClick?: () => void;
}) {
return (
<Dynamic
component={props.onClick ? "button" : "div"}
onClick={props.onClick}
class="flex flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 text-3xl uppercase"
classList={{
"bg-m-grey-800": !props.color && !props.background,
"bg-m-red": props.color === "red" && !props.background,
"bg-m-green": props.color === "green" && !props.background,
"h-[3rem] w-[3rem]": !props.size,
"h-[4rem] w-[4rem]": props.size === "large",
"h-[8rem] w-[8rem]": props.size === "xl",
"active:mt-[1px] active:-mb-[1px]": !!props.onClick
}}
style={{
background: props.background
}}
>
{props.children}
</Dynamic>
);
}
export function LabelCircle(props: {
name?: string;
image_url?: string;
contact: boolean;
label: boolean;
channel?: HackActivityType;
generic?: boolean;
size?: "small" | "large" | "xl";
onClick?: () => void;
}) {
const [gradient] = createResource(async () => {
if (props.name && props.contact) {
@@ -31,11 +62,10 @@ export function LabelCircle(props: {
const [errored, setErrored] = createSignal(false);
return (
<div
class="flex h-[3rem] w-[3rem] flex-none items-center justify-center overflow-clip rounded-full border-b border-t border-b-white/10 border-t-white/50 bg-neutral-700 text-3xl uppercase"
style={{
background: props.image_url && !errored() ? "none" : bg()
}}
<Circle
background={props.image_url && !errored() ? "none" : bg()}
onClick={() => props.onClick && props.onClick()}
size={props.size}
>
<Switch>
<Match when={errored()}>{text()}</Match>
@@ -50,14 +80,11 @@ export function LabelCircle(props: {
}}
/>
</Match>
<Match when={props.channel === "ChannelOpen"}>
<img src={on} alt="channel open" />
</Match>
<Match when={props.channel === "ChannelClose"}>
<img src={off} alt="channel close" />
<Match when={text() === "?" || props.generic}>
<img src={avatar} alt="avatar" />
</Match>
<Match when={true}>{text()}</Match>
</Switch>
</div>
</Circle>
);
}

View File

@@ -15,8 +15,10 @@ function LoadingBar(props: { value: number; max: number }) {
case 2:
return i18n.t("modals.loading.downloading");
case 3:
return i18n.t("modals.loading.setup");
return i18n.t("modals.loading.existing_wallet");
case 4:
return i18n.t("modals.loading.setup");
case 5:
return i18n.t("modals.loading.done");
default:
return i18n.t("modals.loading.default");
@@ -51,10 +53,12 @@ export function LoadingIndicator() {
return 1;
case "downloading":
return 2;
case "setup":
case "checking_for_existing_wallet":
return 3;
case "done":
case "setup":
return 4;
case "done":
return 5;
default:
return 0;
}
@@ -62,7 +66,7 @@ export function LoadingIndicator() {
return (
<Show when={state.load_stage !== "done"}>
<LoadingBar value={loadStageValue()} max={4} />
<LoadingBar value={loadStageValue()} max={5} />
</Show>
);
}

View File

@@ -1,21 +1,14 @@
import { createSignal, JSXElement, ParentComponent } from "solid-js";
import help from "~/assets/icons/help.svg";
import { ExternalLink, SimpleDialog } from "~/components";
import { useI18n } from "~/i18n/context";
export function FeesModal(props: { icon?: boolean }) {
export function FeesModal() {
const i18n = useI18n();
return (
<MoreInfoModal
title={i18n.t("modals.more_info.whats_with_the_fees")}
linkText={
props.icon ? (
<img src={help} alt="help" class="h-4 w-4 cursor-pointer" />
) : (
i18n.t("common.why")
)
}
linkText={i18n.t("common.why")}
>
<p>{i18n.t("modals.more_info.self_custodial")}</p>
<p>{i18n.t("modals.more_info.future_payments")}</p>

View File

@@ -1,7 +1,7 @@
import { A } from "@solidjs/router";
import { ChevronRight } from "lucide-solid";
import { ParentComponent, Show } from "solid-js";
import forward from "~/assets/icons/forward.svg";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
@@ -32,7 +32,7 @@ export function MutinyPlusCta() {
{i18n.t("common.mutiny")}
<span class="text-m-red">+</span>
</span>
<img src={forward} alt="go" />
<ChevronRight />
</div>
<div class="text-sm text-m-grey-400">
<Show

View File

@@ -1,11 +1,13 @@
import { A } from "@solidjs/router";
import airplane from "~/assets/icons/airplane.svg";
import receive from "~/assets/icons/big-receive.svg";
import mutiny_m from "~/assets/icons/m.svg";
import scan from "~/assets/icons/scan.svg";
import settings from "~/assets/icons/settings.svg";
import userClock from "~/assets/icons/user-clock.svg";
import {
ArrowDownLeft,
ArrowUpRight,
Scan,
Settings,
User,
Wallet
} from "lucide-solid";
import { JSX } from "solid-js";
type ActiveTab =
| "home"
@@ -13,14 +15,13 @@ type ActiveTab =
| "send"
| "receive"
| "settings"
| "activity"
| "profile"
| "none";
function NavBarItem(props: {
href: string;
icon: string;
icon: JSX.Element;
active: boolean;
alt: string;
}) {
return (
<li>
@@ -28,11 +29,12 @@ function NavBarItem(props: {
class="block rounded-lg p-2"
href={props.href}
classList={{
"hover:bg-white/5 active:bg-m-blue": !props.active,
"bg-black": props.active
"hover:bg-white/20 active:bg-white/10 active:mt-[2px] active:-mb-[2px]":
!props.active,
"bg-m-red": props.active
}}
>
<img src={props.icon} alt={props.alt} height={36} width={36} />
{props.icon}
</A>
</li>
);
@@ -44,39 +46,33 @@ export function NavBar(props: { activeTab: ActiveTab }) {
<ul class="mt-4 flex flex-col justify-start gap-4 px-4">
<NavBarItem
href="/"
icon={mutiny_m}
icon={<Wallet class="h-8 w-8" />}
active={props.activeTab === "home"}
alt="home"
/>
<NavBarItem
href="/search"
icon={airplane}
icon={<ArrowUpRight class="h-8 w-8" />}
active={props.activeTab === "send"}
alt="send"
/>
<NavBarItem
href="/receive"
icon={receive}
icon={<ArrowDownLeft class="h-8 w-8" />}
active={props.activeTab === "receive"}
alt="receive"
/>
<NavBarItem
href="/activity"
icon={userClock}
active={props.activeTab === "activity"}
alt="activity"
/>
<NavBarItem
href="/scanner"
icon={scan}
icon={<Scan class="h-8 w-8" />}
active={false}
alt="scan"
/>
<NavBarItem
href="/profile"
icon={<User class="h-8 w-8" />}
active={props.activeTab === "profile"}
/>
<NavBarItem
href="/settings"
icon={settings}
icon={<Settings class="h-8 w-8" />}
active={props.activeTab === "settings"}
alt="settings"
/>
</ul>
</nav>

View File

@@ -1,3 +1,6 @@
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
import { useNavigate } from "@solidjs/router";
import { Search } from "lucide-solid";
import {
createEffect,
createResource,
@@ -7,13 +10,14 @@ import {
Switch
} from "solid-js";
import rightArrow from "~/assets/icons/right-arrow.svg";
import { AmountSats, VStack } from "~/components";
import { ButtonCard, NiceP } from "~/components/layout";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { fetchZaps, hexpubFromNpub } from "~/utils";
import { fetchZaps, getPrimalImageUrl } from "~/utils";
import { timeAgo } from "~/utils/prettyPrintTime";
import { GenericItem } from "./GenericItem";
export function Avatar(props: { image_url?: string; large?: boolean }) {
return (
<div
@@ -32,18 +36,12 @@ export function Avatar(props: { image_url?: string; large?: boolean }) {
);
}
function formatProfileLink(hexpub: string): string {
return `https://primal.net/p/${hexpub}`;
}
export function NostrActivity() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
const [data, { refetch }] = createResource(state.npub, fetchZaps);
const [userHexpub] = createResource(state.npub, hexpubFromNpub);
function nameFromHexpub(hexpub: string): string {
const profile = data.latest?.profiles[hexpub];
if (!profile) return hexpub;
@@ -56,8 +54,8 @@ export function NostrActivity() {
const profile = data.latest?.profiles[hexpub];
if (!profile) return;
const parsed = JSON.parse(profile.content);
const image_url = parsed.picture;
return image_url;
const image_url = parsed.image || parsed.picture;
return getPrimalImageUrl(image_url);
}
createEffect(() => {
@@ -67,105 +65,108 @@ export function NostrActivity() {
}
});
const navigate = useNavigate();
// TODO: can this be part of mutiny wallet?
async function newContactFromHexpub(hexpub: string) {
try {
const npub = await MutinyWallet.hexpub_to_npub(hexpub);
if (!npub) {
throw new Error("No npub for that hexpub");
}
const existingContact =
await state.mutiny_wallet?.get_contact_for_npub(npub);
if (existingContact) {
navigate(`/chat/${existingContact.id}`);
return;
}
const profile = data.latest?.profiles[hexpub];
if (!profile) return;
const parsed = JSON.parse(profile.content);
const name = parsed.display_name || parsed.name || profile.pubkey;
const image_url = parsed.image || parsed.picture || undefined;
const ln_address = parsed.lud16 || undefined;
const lnurl = parsed.lud06 || undefined;
const contactId = await state.mutiny_wallet?.create_new_contact(
name,
npub,
ln_address,
lnurl,
image_url
);
if (!contactId) {
throw new Error("no contact id returned");
}
const tagItem = await state.mutiny_wallet?.get_tag_item(contactId);
if (!tagItem) {
throw new Error("no contact returned");
}
navigate(`/chat/${contactId}`);
} catch (e) {
console.error(e);
}
}
return (
<VStack>
<div class="flex w-full flex-col divide-y divide-m-grey-800 overflow-x-clip">
<Show when={!data.latest || data.latest?.zaps.length === 0}>
<ButtonCard onClick={() => navigate("/search")}>
<div class="flex items-center gap-2">
<Search class="inline-block text-m-red" />
<NiceP>{i18n.t("home.find")}</NiceP>
</div>
</ButtonCard>
</Show>
<For each={data.latest?.zaps}>
{(zap) => (
<div
class="rounded-lg bg-m-grey-800 p-2"
classList={{
"outline outline-m-blue":
userHexpub() === zap.to_hexpub
}}
>
<div class="grid grid-cols-[1fr_auto_1fr] gap-4">
<div class="grid gap-2 sm:grid-cols-[auto_1fr] sm:items-center">
<Avatar
image_url={imageFromHexpub(zap.from_hexpub)}
/>
<span class="truncate whitespace-nowrap text-left text-sm font-semibold uppercase">
<Switch>
<Match when={zap.kind === "public"}>
<a
href={formatProfileLink(
zap.from_hexpub
)}
target="_blank"
rel="noopener noreferrer"
class="no-underline"
>
{nameFromHexpub(
zap.from_hexpub
)}
</a>
</Match>
<Match when={zap.kind === "private"}>
{i18n.t("activity.private")}
</Match>
<Match when={zap.kind === "anonymous"}>
{i18n.t("activity.anonymous")}
</Match>
</Switch>
</span>
</div>
<div class="flex flex-col items-center justify-center">
<div class="flex items-center gap-1">
<AmountSats amountSats={zap.amount_sats} />
<img
src={rightArrow}
alt="right arrow"
class="h-4 w-4"
/>
</div>
<time class="text-sm text-m-grey-400">
<Show
when={zap.event_id}
fallback={timeAgo(
zap.timestamp,
data.latest?.until
)}
>
<a
href={`https://primal.net/e/${zap.event_id}`}
target="_blank"
rel="noopener noreferrer"
>
{timeAgo(
zap.timestamp,
data.latest?.until
)}
</a>
</Show>
</time>
</div>
<div class="grid gap-2 self-end sm:grid-cols-[1fr_auto] sm:items-center ">
<div class="self-right flex justify-end">
<Avatar
image_url={imageFromHexpub(
zap.to_hexpub
)}
/>
</div>
<a
href={formatProfileLink(zap.to_hexpub)}
class="truncate whitespace-nowrap text-right text-sm font-semibold uppercase no-underline sm:-order-1 sm:text-right"
target="_blank"
rel="noopener noreferrer"
>
{nameFromHexpub(zap.to_hexpub)}
</a>
</div>
</div>
<Show when={zap.content}>
<hr class="my-2 border-m-grey-750" />
<p
class="truncate text-center text-sm font-light text-neutral-200"
textContent={zap.content}
/>
</Show>
</div>
<>
<GenericItem
primaryAvatarUrl={
imageFromHexpub(zap.from_hexpub) || ""
}
primaryName={
zap.kind === "anonymous"
? i18n.t("activity.anonymous")
: zap.kind === "private"
? i18n.t("activity.private")
: nameFromHexpub(zap.from_hexpub)
}
primaryOnClick={() => {
newContactFromHexpub(zap.from_hexpub);
}}
secondaryAvatarUrl={
imageFromHexpub(zap.to_hexpub) || ""
}
secondaryName={nameFromHexpub(zap.to_hexpub)}
secondaryOnClick={() => {
newContactFromHexpub(zap.to_hexpub);
}}
verb={"zapped"}
amount={zap.amount_sats}
message={zap.content ? zap.content : undefined}
date={timeAgo(zap.timestamp, data.latest?.until)}
visibility={
zap.kind === "public" ? "public" : "private"
}
genericAvatar={
zap.kind === "anonymous" ||
zap.kind === "private"
}
forceSecondary
link={`https://njump.me/e/${zap.event_id}`}
/>
</>
)}
</For>
</VStack>
</div>
);
}

View File

@@ -1,37 +0,0 @@
import { Show } from "solid-js";
import save from "~/assets/icons/save.svg";
import { ButtonLink, SmallHeader } from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
export function OnboardWarning() {
const i18n = useI18n();
const [state, _actions] = useMegaStore();
return (
<>
<Show when={!state.has_backed_up}>
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] gap-4 rounded-xl bg-neutral-950/50 p-4">
<div class="self-center">
<img src={save} alt="backup" class="h-8 w-8" />
</div>
<div class="flex flex-col justify-center">
<SmallHeader>
{i18n.t("modals.onboarding.secure_your_funds")}
</SmallHeader>
</div>
<div class="flex items-center">
<ButtonLink
intent="blue"
layout="xs"
href="/settings/backup"
>
{i18n.t("settings.backup.title")}
</ButtonLink>
</div>
</div>
</Show>
</>
);
}

View File

@@ -1,3 +1,6 @@
import { TagItem } from "@mutinywallet/mutiny-wasm";
import { useNavigate } from "@solidjs/router";
import { Check, PlugZap, X } from "lucide-solid";
import {
createEffect,
createResource,
@@ -8,22 +11,13 @@ import {
Switch
} from "solid-js";
import bolt from "~/assets/icons/bolt.svg";
import greenCheck from "~/assets/icons/green-check.svg";
import redClose from "~/assets/icons/red-close.svg";
import {
ActivityAmount,
Card,
InfoBox,
LoadingSpinner,
VStack
} from "~/components";
import { ButtonCard, GenericItem, InfoBox, NiceP } from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import {
createDeepSignal,
eify,
formatExpiration,
veryShortTimeStamp,
vibrateSuccess
} from "~/utils";
@@ -40,10 +34,16 @@ export function PendingNwc() {
const [error, setError] = createSignal<Error>();
const navigate = useNavigate();
async function fetchPendingRequests() {
const profiles = await state.mutiny_wallet?.get_nwc_profiles();
if (!profiles) return [];
const contacts: TagItem[] | undefined =
await state.mutiny_wallet?.get_contacts_sorted();
if (!contacts) return [];
const pending = await state.mutiny_wallet?.get_pending_nwc_invoices();
if (!pending) return [];
@@ -59,6 +59,16 @@ export function PendingNwc() {
date: p.expiry,
amount_sats: p.amount_sats
});
} else {
const contact = contacts.find((c) => c.npub === p.npub);
if (contact) {
pendingItems.push({
id: p.id,
name_of_connection: contact.name,
date: p.expiry,
amount_sats: p.amount_sats
});
}
}
}
return pendingItems;
@@ -70,11 +80,12 @@ export function PendingNwc() {
{ storage: createDeepSignal }
);
const [paying, setPaying] = createSignal<string>("");
const [payList, setPayList] = createSignal<string[]>([]);
async function payItem(item: PendingItem) {
try {
setPaying(item.id);
// setPaying(item.id);
setPayList([...payList(), item.id]);
await state.mutiny_wallet?.approve_invoice(item.id);
await vibrateSuccess();
} catch (e) {
@@ -93,7 +104,7 @@ export function PendingNwc() {
console.error(e);
}
} finally {
setPaying("");
setPayList(payList().filter((id) => id !== item.id));
refetch();
}
}
@@ -119,13 +130,13 @@ export function PendingNwc() {
async function rejectItem(item: PendingItem) {
try {
setPaying(item.id);
setPayList([...payList(), item.id]);
await state.mutiny_wallet?.deny_invoice(item.id);
} catch (e) {
setError(eify(e));
console.error(e);
} finally {
setPaying("");
setPayList(payList().filter((id) => id !== item.id));
refetch();
}
}
@@ -147,92 +158,70 @@ export function PendingNwc() {
});
return (
<Show when={pendingRequests() && pendingRequests()!.length > 0}>
<Card title={i18n.t("settings.connections.pending_nwc.title")}>
<div class="p-1" />
<VStack>
<Switch>
<Match when={pendingRequests() && pendingRequests()!.length > 0}>
<ButtonCard onClick={() => navigate("/settings/connections")}>
<div class="flex items-center gap-2">
<PlugZap class="inline-block text-m-red" />
<NiceP>{i18n.t("home.connection_edit")}</NiceP>
</div>
</ButtonCard>
<div class="flex w-full justify-around">
<button
class="flex items-center gap-1 font-semibold text-m-green active:-mb-[1px] active:mt-[1px] active:text-m-green/80"
onClick={approveAll}
>
<Check />
<span>
{i18n.t(
"settings.connections.pending_nwc.approve_all"
)}
</span>
</button>
<button
class="flex items-center gap-1 font-semibold text-m-red active:-mb-[1px] active:mt-[1px] active:text-m-red/80"
onClick={denyAll}
>
<X />
<span>
{i18n.t(
"settings.connections.pending_nwc.deny_all"
)}
</span>
</button>
</div>
<div class="flex w-full flex-col divide-y divide-m-grey-800 overflow-x-clip">
<Show when={error()}>
<InfoBox accent="red">{error()?.message}</InfoBox>
</Show>
<For each={pendingRequests()}>
{(pendingItem) => (
<div class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)_auto] items-center gap-4 border-b border-neutral-800 pb-4 last:border-b-0">
<img
class="w-[1rem]"
src={bolt}
alt="onchain"
/>
<div class="flex flex-col">
<span class="truncate text-base font-semibold">
{pendingItem.name_of_connection}
</span>
<time class="text-sm text-neutral-500">
{formatExpiration(pendingItem.date)}
</time>
</div>
<div>
<ActivityAmount
amount={
pendingItem.amount_sats?.toString() ||
"0"
}
price={state.price}
/>
</div>
<div class="flex w-[5rem] gap-2">
<Switch>
<Match
when={paying() !== pendingItem.id}
>
<button
onClick={() =>
payItem(pendingItem)
}
>
<img
class="h-[2.5rem] w-[2.5rem]"
src={greenCheck}
alt="Approve"
/>
</button>
<button
onClick={() =>
rejectItem(pendingItem)
}
>
<img
class="h-[2rem] w-[2rem]"
src={redClose}
alt="Reject"
/>
</button>
</Match>
<Match
when={paying() === pendingItem.id}
>
<LoadingSpinner wide />
</Match>
</Switch>
</div>
</div>
<GenericItem
primaryAvatarUrl=""
verb="requested"
amount={pendingItem.amount_sats || 0n}
due={veryShortTimeStamp(pendingItem.date)}
genericAvatar
primaryName={pendingItem.name_of_connection}
approveAction={() => payItem(pendingItem)}
rejectAction={() => rejectItem(pendingItem)}
shouldSpinny={payList().includes(
pendingItem.id
)}
/>
)}
</For>
</VStack>
<div class="flex w-full justify-around">
<button
class="font-semibold text-m-green active:text-m-red/80"
onClick={approveAll}
>
{i18n.t("settings.connections.pending_nwc.approve_all")}
</button>
<button
class="font-semibold text-m-red active:text-m-red/80"
onClick={denyAll}
>
{i18n.t("settings.connections.pending_nwc.deny_all")}
</button>
</div>
</Card>
</Show>
</Match>
<Match when={true}>
<ButtonCard onClick={() => navigate("/settings/connections")}>
<div class="flex items-center gap-2">
<PlugZap class="inline-block text-m-red" />
<NiceP>{i18n.t("home.connection")}</NiceP>
</div>
</ButtonCard>
</Match>
</Switch>
);
}

View File

@@ -8,7 +8,7 @@ import { Capacitor } from "@capacitor/core";
import QrScanner from "qr-scanner";
import { onCleanup, onMount } from "solid-js";
export function Scanner(props: { onResult: (result: string) => void }) {
export function Reader(props: { onResult: (result: string) => void }) {
let container: HTMLVideoElement | undefined;
let scanner: QrScanner | undefined;

Some files were not shown because too many files have changed in this diff Show More