diff --git a/README.md b/README.md index 22ec284..3392881 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ A Graphical User Interface (GUI) for - [ ] Fix any bugs that arise ### Stage 2 +- [ ] Add "Max" option to send dialog #41 +- [ ] What about putting data into a normal web link (https://wallet.dashincubator.dev with parameters in the query string) that people can just click and accept your friend request? - [ ] PWA Improvements & Fixes - [ ] Fix White Screen of Death - [ ] Mobile Improvements @@ -64,6 +66,9 @@ A Graphical User Interface (GUI) for - [ ] Desktop / Tauri Wrapper - [ ] Mobile / Capacitor App +- [ ] feat: I kind of want some hover options right on each contact in the list. This would bring some life and color to the otherwise sparse list. + - [ ] trash icon - quickly get rid of contacts that I know I didn’t share + ### Stage 2 - QA - [ ] @@ -94,3 +99,20 @@ A Graphical User Interface (GUI) for ### Stage 4 - QA - [ ] + + +### Backlog +- [ ] style(julius): Is there some reason it is necessary to use the terms “disconnect” and “add wallet” as opposed to just “log in” and “log out”? +- [ ] Tutorializing for the user + - [ ] doc(julius): Minor aspects of user understanding: how does the wallet actually work? What information is your browser storing when you create a wallet? Is it downloading a temporary self custodial wallet into the browser session? How does that work, where is it saved to? How does locking work? +- [ ] doc(julius): User experience: should you be able to create a new alias every time you log back in? +- [ ] style(julius): Minor visual design notes: the landing page feels slightly “empty”. Awkward side columns. +- [ ] feat: Maybe we should use the big empty space in the contacts list to recommend pairing with other prominent wallets (mobile wallet, Desktop wallet, etc). + - [ ] DCG’s mobile wallet definitely needs to display a QR for its xpub address. + - [ ] We need to support importing raw xpubs if we don’t already. +- [ ] style: Top button is “adjective noun”, bottom two buttons are “verb noun”. Maybe make it consistent. +- [ ] feat: Every time I click Request the wallet generates a new address. This might be unsettling for users. Need to rethink this functionality. Side note/question: Is this cycling through the 0 index on the HD path when no contact is selected? Either way, we may want to put our own username in the alias input box by default, which would be the default funding mechanism. +- [ ] Contact Data List Selector in the Send Dialog on mobile does not show contact alias hints + - Appears to be an issue specific to Firefox on Android, both normal version (in normal & private view) & Firefox Focus +- [ ] Edit Profile, Add Contact & Insufficient Wallet Funds Dialogs have layout issues on mobile +- [ ] diff --git a/TODO.md b/TODO.md index bc4c2e7..2bb72ff 100644 --- a/TODO.md +++ b/TODO.md @@ -4,19 +4,22 @@ - [x] QR Code - [x] Scan - [ ] Upload QR image - - [x] Modify `src/rigs/send-or-request.js` to toggle between send & request, not show both - - [x] Send Dialog - - [x] Request Dialog - [ ] Fiat balance from: - https://rates2.dashretail.org/rates?source=dashretail&%7B%7D= - symbol=DASH${symbol} - [ ] Styled Drop Down List - [ ] Type Ahead Input Field -- [x] Batch generate IndexedDB store addresses on wallet load and after addressIndex is incremented by requesting funds in Send / Request dialog -- [x] Clicking send with a Zero balance throws an error in console - - Need dialog/error message to indicate what/why it won't send -- [x] On Pairing/Editing contacts: - - [x] Enforce unique Aliases - - [x] Should be able to add a contact by normal Dash Address (without Dash URI DIP: aj-contact-scanback features) - - [x] Check if XkeyID exists -- [x] Add `updatedAt` property to IndexedDB Stores \ No newline at end of file + + +### Bugs +#### Mobile Specific +- [ ] + +### Enhancements +- [ ] Batch Generate tweaks + - need to check each address funds and generate 20 past the last address with funds + - need to pre-generate accounts with some addresses when importing a phrase to start adding contacts after the last account with funds + +#### General +- [ ] Dialog: Send Error Messages + - we need to surface errors in the UI on send failure diff --git a/package-lock.json b/package-lock.json index 8b113b7..edfbb45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "dashphrase": "^1.4.0", "dashsight": "^1.6.1", "dashtx": "^0.13.2", - "dashwallet": "^0.6.0", + "dashwallet": "^0.6.1", "html5-qrcode": "^2.3.8", "idb": "^8.0.0", "localforage": "^1.10.0", @@ -134,9 +134,9 @@ } }, "node_modules/dashwallet": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/dashwallet/-/dashwallet-0.6.0.tgz", - "integrity": "sha512-rg510kNWJ2q0a2pBiW+NBG5ivzpzu9i7mm86rqDG302KwVlvryTyx4YfJKc8Q577gtyMEbEbDUQa4Ifb6dDVzg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/dashwallet/-/dashwallet-0.6.1.tgz", + "integrity": "sha512-5Gugr6Mv519Dl8Sp8roqvu2QuXaf8YtylbEeE94Nw42VuszZ5rJNcfHAuLBY8b6oMG3iazD1RXRYPaBYBDqXvA==", "dependencies": { "dashhd": "^3.0.5", "dashphrase": "^1.3.9", diff --git a/package.json b/package.json index 7f83b86..1259c24 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "dashphrase": "^1.4.0", "dashsight": "^1.6.1", "dashtx": "^0.13.2", - "dashwallet": "^0.6.0", + "dashwallet": "^0.6.1", "html5-qrcode": "^2.3.8", "idb": "^8.0.0", "localforage": "^1.10.0", diff --git a/src/components/contacts-list.js b/src/components/contacts-list.js index 2d29540..01487aa 100644 --- a/src/components/contacts-list.js +++ b/src/components/contacts-list.js @@ -38,13 +38,13 @@ const initialState = { `, - content: state => html` + content: async state => html` ${state.header(state)}
${ state.contacts.length > 0 - ? state.contacts.map(c => state.item(c)).join('') + ? (await Promise.all(state.contacts.map(async c => await state.item(c)))).join('') : '' } ${ @@ -53,9 +53,9 @@ const initialState = { }
- ${state.footer(state)} + ${await state.footer(state)} `, - item: c => { + item: async c => { // console.warn('contact list item', c) if ('string' === typeof c) { return html` @@ -94,27 +94,55 @@ const initialState = { let inId = Object.keys(c?.incoming || {})?.[0]?.split('/')[1] + let atUser = user + ? `@${user}` + : '' let itemAlias = user - ? `@${user}${ !paired ? ' - ' : '' }${finishPairing}` + ? `${atUser}${ !paired ? ' - ' : '' }${finishPairing}` : finishPairing || enterContactInfo let itemName = name ? `${name}` : '' let itemSub = inId - ? `data-id="${inId}"` + ? `href="/#!/contact/${atUser || inId}" data-id="${inId}"` : '' return html` -
- ${getAvatar(c)} + + ${await getAvatar(c)}

${itemAlias}

${itemName}
-
+ ` }, - footer: state => html``, + footer: async state => html` + + ${ + state.contacts.length > 0 + ? ( + await Promise.all( + state.contacts + .filter( + c => c.alias && + Object.keys(c.outgoing || {}).length > 0 + ).map(contact => { + return html`` + }) + // .map( + // async c => await state.item(c) + // ) + ) + ).join('') + : '' + } + + `, slugs: { }, elements: { @@ -178,7 +206,7 @@ export async function setupContactsList( section.id = state.slugs.section section.classList.add(state.placement || '') - section.innerHTML = state.content(state) + section.innerHTML = await state.content(state) function addListener( node, @@ -225,7 +253,7 @@ export async function setupContactsList( await restate(state, renderState) section.id = state.slugs.section - section.innerHTML = state.content(state) + section.innerHTML = await state.content(state) removeAllListeners() addListeners() diff --git a/src/components/dialog.js b/src/components/dialog.js index ddda3c5..881f19c 100644 --- a/src/components/dialog.js +++ b/src/components/dialog.js @@ -1,5 +1,14 @@ import { lit as html } from '../helpers/lit.js' -import { formDataEntries } from '../helpers/utils.js' +import { + formDataEntries, + envoy, +} from '../helpers/utils.js' + +let modal = envoy( + { + rendered: {}, + }, +) let _handlers = [] @@ -13,10 +22,11 @@ const initialState = { closeTxt: 'X', closeAlt: `Close`, placement: 'center', + modal, rendered: null, responsive: true, delay: 500, - render () {}, + async render () {}, addListener () {}, addListeners () {}, removeAllListeners (targets) {}, @@ -28,7 +38,7 @@ const initialState = { } `, - content: state => html` + content: async state => html` ${state.header(state)}
@@ -72,6 +82,7 @@ const initialState = { placeholder="Do Something" minlength="1" spellcheck="false" + autocomplete="off" />

Some instructions

@@ -82,11 +93,21 @@ const initialState = { }, events: { handleInput: state => event => { - event.preventDefault() + // let { + // // @ts-ignore + // name: fieldName, form, + // } = event?.target + + // console.log('handle input', { + // fieldName, + // form, + // }) + if ( event?.target?.validity?.patternMismatch && event?.target?.type !== 'checkbox' ) { + event.preventDefault() let label = event.target?.previousElementSibling?.textContent?.trim() if (label) { event.target.setCustomValidity(`Invalid ${label}`) @@ -96,25 +117,22 @@ const initialState = { } event.target.reportValidity() }, - handleFocus: state => event => { - // event.preventDefault() - // console.log( - // 'handle input focus', - // event, - // ) - }, - handleDrop: state => event => { - event.preventDefault() - }, - handleDragOver: state => event => { - event.preventDefault() - }, handleChange: state => event => { - event.preventDefault() + // let { + // // @ts-ignore + // name: fieldName, form, + // } = event?.target + + // console.log('handle change', { + // fieldName, + // form, + // }) + if ( event?.target?.validity?.patternMismatch && event?.target?.type !== 'checkbox' ) { + event.preventDefault() let label = event.target?.previousElementSibling?.textContent?.trim() if (label) { event.target.setCustomValidity(`Invalid ${label}`) @@ -124,16 +142,60 @@ const initialState = { } event.target.reportValidity() }, + handleBlur: state => event => { + // event.preventDefault() + // console.log( + // 'handle blur', + // event, + // ) + }, + handleFocusOut: state => event => { + // event.preventDefault() + // console.log( + // 'handle focus out', + // event, + // ) + }, + handleFocusIn: state => event => { + // event.preventDefault() + // console.log( + // 'handle focus in', + // event, + // ) + }, + handleDrop: state => event => { + event.preventDefault() + }, + handleDragOver: state => event => { + event.preventDefault() + }, + handleDragEnd: state => event => { + event.preventDefault() + }, + handleDragLeave: state => event => { + event.preventDefault() + }, handleRender: ( state, - // resolve = res=>{}, - // reject = res=>{}, ) => { // console.log( // 'handle dialog render', // state, // ) }, + handleShow: ( + state, + ) => { + // console.log( + // 'handle dialog show', + // state, + // ) + + // focus first input + state.elements.form.querySelector( + 'input' + )?.focus() + }, handleClose: ( state, resolve = res=>{}, @@ -154,10 +216,20 @@ const initialState = { } else { resolve('cancel') } + // console.log( + // 'DIALOG handleClose', + // modal.rendered[state.slugs.dialog], + // ) setTimeout(t => { - state.rendered = null + modal.rendered[state.slugs.dialog] = null event?.target?.remove() + // console.log( + // 'DIALOG handleClose setTimeout', + // state.delay, + // // modal.rendered[state.slugs.dialog], + // modal.rendered, + // ) }, state.delay) }, handleSubmit: state => event => { @@ -176,7 +248,10 @@ const initialState = { }, handleReset: state => event => { event.preventDefault() - state.elements.form?.removeEventListener('close', state.events.handleReset) + state.elements.form?.removeEventListener( + 'close', + state.events.handleReset + ) // console.log( // 'handleReset', // [event.target], @@ -196,7 +271,7 @@ const initialState = { }, } -export function setupDialog( +export async function setupDialog( el, setupState = {} ) { let state = { @@ -242,7 +317,7 @@ export function setupDialog( form.name = `${state.slugs.form}` form.method = 'dialog' - form.innerHTML = state.content(state) + form.innerHTML = await state.content(state) dialog.insertAdjacentElement( 'afterbegin', @@ -263,37 +338,64 @@ export function setupDialog( resolve, reject, ) { - addListener( - dialog, - 'close', - state.events.handleClose(state, resolve, reject), - ) - addListener( - dialog, - 'click', - state.events.handleClick(state), - ) + if (resolve && reject) { + addListener( + dialog, + 'close', + state.events.handleClose(state, resolve, reject), + ) + + addListener( + dialog, + 'click', + state.events.handleClick(state), + ) + } addListener( form, - 'focusout', - state.events.handleFocus(state), + 'blur', + state.events.handleBlur(state), ) addListener( form, - 'change', - state.events.handleChange(state), + 'focusout', + state.events.handleFocusOut(state), ) addListener( form, - 'drop', - state.events.handleDrop(state), + 'focusin', + state.events.handleFocusIn(state), ) addListener( form, - 'dragover', - state.events.handleDragOver(state), + 'change', + state.events.handleChange(state), ) + // let updrop = form.querySelector('.updrop') + // state.elements.updrop = updrop + // if (updrop) { + addListener( + form, + 'drop', + state.events.handleDrop(state), + ) + addListener( + form, + 'dragover', + state.events.handleDragOver(state), + ) + addListener( + form, + 'dragend', + state.events.handleDragEnd(state), + ) + addListener( + form, + 'dragleave', + state.events.handleDragLeave(state), + ) + // } addListener( form, 'input', @@ -311,9 +413,14 @@ export function setupDialog( ) } + state.addListeners = addListeners + function removeAllListeners( targets = [dialog,form], ) { + if (state.elements.updrop) { + targets.push(state.elements.updrop) + } _handlers = _handlers .filter(({ node, event, handler, capture }) => { if (targets.includes(node)) { @@ -351,13 +458,13 @@ export function setupDialog( dialog.id = state.slugs.dialog form.name = `${state.slugs.form}` - form.innerHTML = state.content(state) + form.innerHTML = await state.content(state) - // console.log('DIALOG RENDER', state, position, state.slugs.dialog) + // console.log('DIALOG RENDER', state, position, state.slugs.dialog, modal.rendered) - if (!state.rendered) { + if (!modal.rendered[state.slugs.dialog]) { el.insertAdjacentElement(position, dialog) - state.rendered = dialog + modal.rendered[state.slugs.dialog] = dialog } state.events.handleRender(state) @@ -370,13 +477,17 @@ export function setupDialog( show: (callback) => new Promise((resolve, reject) => { removeAllListeners() addListeners(resolve, reject) + // console.log('dialog show', dialog) dialog.show() + state.events.handleShow?.(state) callback?.() }), showModal: (callback) => new Promise((resolve, reject) => { removeAllListeners() addListeners(resolve, reject) + // console.log('dialog showModal', dialog) dialog.showModal() + state.events.handleShow?.(state) callback?.() }), close: returnVal => dialog.close(returnVal), diff --git a/src/components/input-amount.js b/src/components/input-amount.js deleted file mode 100644 index f0f8e0c..0000000 --- a/src/components/input-amount.js +++ /dev/null @@ -1,181 +0,0 @@ -import { lit as html } from '../helpers/lit.js' - -const initialState = { - id: 'Input', - name: 'Amount', - placement: 'field', - rendered: null, - responsive: true, - delay: 500, - content: state => html` - -
- - -
- -
- `, - slugs: { - }, - elements: { - }, - events: { - handleChange: state => event => { - event.preventDefault() - console.log( - 'handle amount change', - event?.target?.validationMessage, - event?.target?.validity, - [event.target], - event?.target?.type - ) - // if ( - // event?.target?.validity?.patternMismatch && - // event?.target?.type !== 'checkbox' - // ) { - // console.log( - // 'handle funds change', - // event?.target?.validationMessage, - // event?.target?.validity, - // [event.target], - // event?.target?.type - // ) - // let label = event.target?.previousElementSibling?.textContent?.trim() - // if (label) { - // event.target.setCustomValidity(`Invalid ${label}`) - // } - // // event.target.reportValidity() - // } else if (event?.target?.validity?.valid) { - // event.target.setCustomValidity('') - // } - // event.target.reportValidity() - }, - handleClick: state => event => { - if (event.target === state.elements.funds) { - console.log( - 'handle funds backdrop click', - event, - event.target === state.elements.funds - ) - } - } - }, -} - -export function setupInputAmount( - el, setupState = {} -) { - let state = { - ...initialState, - ...setupState, - slugs: { - ...initialState.slugs, - ...setupState.slugs, - }, - events: { - ...initialState.events, - ...setupState.events, - }, - elements: { - ...initialState.elements, - ...setupState.elements, - } - } - - state.slugs.fieldset = `${state.name}_${state.id}`.toLowerCase().replace(' ', '_') - - const fieldset = document.createElement('div') - - state.elements.fieldset = fieldset - - fieldset.id = state.slugs.fieldset - fieldset.classList.add(state.placement) - fieldset.innerHTML = state.content(state) - - fieldset.addEventListener( - 'click', - state.events.handleClick(state) - ) - fieldset.addEventListener( - 'change', - state.events.handleChange(state) - ) - - return { - element: fieldset, - renderAsHTML: ( - renderState = {}, - position = 'beforebegin' - ) => { - state = { - ...state, - ...renderState, - slugs: { - ...state.slugs, - ...renderState.slugs, - }, - events: { - ...state.events, - ...renderState.events, - }, - elements: { - ...state.elements, - ...renderState.elements, - } - } - - fieldset.id = state.slugs.fieldset - // fieldset.name = `${state.slugs.fieldset}` - fieldset.innerHTML = state.content(state) - - return fieldset.outerHTML - }, - render: ( - renderState = {}, - position = 'afterend', - ) => { - state = { - ...state, - ...renderState, - slugs: { - ...state.slugs, - ...renderState.slugs, - }, - events: { - ...state.events, - ...renderState.events, - }, - elements: { - ...state.elements, - ...renderState.elements, - } - } - - fieldset.id = state.slugs.fieldset - // fieldset.name = `${state.slugs.fieldset}` - fieldset.innerHTML = state.content(state) - - if (!state.rendered) { - el.insertAdjacentElement(position, fieldset) - state.rendered = fieldset - } - } - } -} - -export default setupInputAmount diff --git a/src/components/nav.js b/src/components/nav.js index 876a4d8..1b25755 100644 --- a/src/components/nav.js +++ b/src/components/nav.js @@ -14,7 +14,7 @@ const initialState = {
  • - @${state.data?.alias} + @${state.data?.alias}