Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Advanced UI features #133

Merged
merged 11 commits into from
Apr 1, 2021
355 changes: 217 additions & 138 deletions frontend/src/components/AccountReceiveTable.vue

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/src/components/BaseInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export default Vue.extend({

hideHint() {
this.hintString = '';
this.$emit('blur', this.content);
},

showHint() {
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/css/app.sass
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ p, div
text-align: center

.form
max-width: 510px
max-width: 475px
margin: 0 auto

.form-wide
// Slightly wider form to ensure withdrawal addresses and private keys don't wrap on the receive table
max-width: 525px
margin: 0 auto

.horizontal-center
Expand Down
72 changes: 40 additions & 32 deletions frontend/src/layouts/BaseLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,13 @@

<!-- ADDRESS AND SETTINGS AND SETTINGS -->
<div class="col-auto q-mr-md">
<div v-if="userDisplayName" class="text-caption dark-toggle">
{{ userDisplayName }}
<div>
<span v-if="userDisplayName" class="text-caption dark-toggle">
{{ userDisplayName }}
</span>
<span v-if="advancedMode" class="q-ml-md">
🧙 <q-tooltip content-class="bg-muted dark-toggle shadow-2 q-pa-md"> Advanced mode is on </q-tooltip>
</span>
</div>
</div>
</div>
Expand All @@ -71,29 +76,44 @@

<q-footer class="q-mx-md q-mb-md q-pt-xl" style="color: #000000; background-color: rgba(0, 0, 0, 0)">
<div class="row justify-between">
<div class="col-auto">
<!-- Column 1: User settings -->
<div class="col">
<!-- Dark mode toggle -->
<q-icon
v-if="!$q.dark.isActive"
class="dark-toggle"
@click="toggleDarkMode"
class="dark-toggle cursor-pointer"
name="fas fa-moon"
size="xs"
style="cursor: pointer"
@click="toggleDarkMode()"
/>
<q-icon
v-else
class="dark-toggle"
name="fas fa-sun"
size="xs"
style="cursor: pointer"
@click="toggleDarkMode()"
<q-icon v-else @click="toggleDarkMode" class="dark-toggle cursor-pointer" name="fas fa-sun" />

<!-- Advanced mode toggle -->
<q-toggle
@input="toggleAdvancedMode"
:value="advancedMode"
class="q-ml-lg"
color="primary"
icon="fas fa-cog"
/>
<span class="dark-toggle text-caption">Advanced mode {{ advancedMode ? 'on' : 'off' }}</span>
<span>
<q-icon class="dark-toggle" right name="fas fa-question-circle">
<q-tooltip content-class="bg-muted dark-toggle shadow-2 q-pa-md" max-width="14rem">
Enables advanced features such as private key export, additional recipient ID options, and event
scanning settings. <span class="text-bold">Use with caution!</span>
</q-tooltip>
</q-icon>
</span>
</div>
<div class="col-auto text-caption">

<!-- Column 2: Built by ScopeLift -->
<div class="col text-center text-caption">
Built by
<a href="https://www.scopelift.co/" target="_blank" class="hyperlink">ScopeLift</a>
</div>
<div class="col-auto">

<!-- Column 3: Links -->
<div class="col text-right">
<a href="https://twitter.com/UmbraCash" target="_blank" class="no-text-decoration">
<q-icon class="dark-toggle" name="fab fa-twitter" size="xs" />
</a>
Expand All @@ -110,23 +130,10 @@
</template>

<script lang="ts">
import { defineComponent, onMounted, ref, watchEffect } from '@vue/composition-api';
import { Dark, LocalStorage } from 'quasar';
import { defineComponent, ref, watchEffect } from '@vue/composition-api';
import useSettingsStore from 'src/store/settings';
import useWalletStore from 'src/store/wallet';

function useDarkMode() {
function toggleDarkMode() {
Dark.set(!Dark.isActive);
LocalStorage.set('is-dark', Dark.isActive);
}

const mounted = onMounted(function () {
Dark.set(Boolean(LocalStorage.getItem('is-dark')));
});

return { toggleDarkMode, mounted };
}

function useWallet() {
const { userDisplayName, network } = useWalletStore();
const networkName = ref('');
Expand All @@ -143,7 +150,8 @@ function useWallet() {
export default defineComponent({
name: 'BaseLayout',
setup() {
return { ...useDarkMode(), ...useWallet() };
const { advancedMode, toggleAdvancedMode, isDark, toggleDarkMode } = useSettingsStore();
return { advancedMode, toggleAdvancedMode, isDark, toggleDarkMode, ...useWallet() };
},
});
</script>
141 changes: 120 additions & 21 deletions frontend/src/pages/AccountReceive.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,49 @@

<div v-else class="q-mx-auto" style="max-width: 800px">
<!-- Waiting for signature -->
<div v-if="!hasKeys" class="form">
<div class="text-center q-mb-md">This app needs your signature to scan for funds you've received</div>
<base-button @click="getPrivateKeysHandler" class="text-center" label="Sign" />
<div v-if="needsSignature || scanStatus === 'waiting'" class="form">
<div v-if="needsSignature" class="text-center q-mb-md">
This app needs your signature to scan for funds you've received
</div>
<div v-else class="text-center q-mb-md">Click below to scan for funds you've received</div>
<base-button @click="getPrivateKeysHandler" class="text-center" :label="needsSignature ? 'Sign' : 'Scan'" />

<!-- Advanced mode settings -->
<q-card v-if="advancedMode" class="q-pt-md q-px-md q-mt-xl">
<q-card-section class="text-center text-primary text-h6 header-black q-pb-none">
Scan Settings
</q-card-section>
<q-card-section>
<q-form class="text-left" ref="settingsFormRef">
<div>
Enter the start or end blocks to use when scanning for events. A blank start block will scan from block
zero, and a blank end block will scan through the current block.
</div>
<div class="row justify-start">
<base-input
v-model.number="startBlockLocal"
@blur="setScanBlocks(startBlockLocal, endBlockLocal)"
class="col-5"
label="Start block"
:rules="isValidStartBlock"
/>
<base-input
v-model.number="endBlockLocal"
@blur="setScanBlocks(startBlockLocal, endBlockLocal)"
class="col-5 q-ml-md"
label="End block"
:rules="isValidEndBlock"
/>
</div>
<div>
Enter the private key to use when scanning for events. A blank private key will use the ones generated
from your signature.
</div>
<!-- Unlike start blocks, no action on blur because we don't want to save private key to LocalStorage -->
<base-input v-model="scanPrivateKeyLocal" label="Private key" :rules="isValidPrivateKey" />
</q-form>
</q-card-section>
</q-card>
</div>

<!-- Scanning in progress -->
Expand All @@ -22,51 +62,110 @@

<!-- Scanning complete -->
<div v-else-if="scanStatus === 'complete'" class="text-center">
<account-receive-table :announcements="userAnnouncements" />
<account-receive-table :announcements="userAnnouncements" @reset="setFormStatus('waiting', '')" />
</div>
</div>
</q-page>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from '@vue/composition-api';
import { computed, defineComponent, onMounted, ref } from '@vue/composition-api';
import { QForm } from 'quasar';
import { UserAnnouncement } from '@umbra/umbra-js';
import { isHexString } from '@ethersproject/bytes';
import useSettingsStore from 'src/store/settings';
import useWallet from 'src/store/wallet';
import AccountReceiveTable from 'components/AccountReceiveTable.vue';
import ConnectWalletCard from 'components/ConnectWalletCard.vue';

function useScan() {
const { getPrivateKeys, umbra, spendingKeyPair, viewingKeyPair, hasKeys, userAddress } = useWallet();
const scanStatus = ref<'waiting' | 'scanning' | 'complete'>('waiting');
type ScanStatus = 'waiting' | 'scanning' | 'complete';
const scanStatus = ref<ScanStatus>('waiting');
const userAnnouncements = ref<UserAnnouncement[]>([]);

// Start and end blocks for advanced mode settings
const { advancedMode, startBlock, endBlock, setScanBlocks, setScanPrivateKey } = useSettingsStore();
const startBlockLocal = ref<number>();
const endBlockLocal = ref<number>();
const scanPrivateKeyLocal = ref<string>();
const needsSignature = computed(() => !hasKeys.value && !scanPrivateKeyLocal.value);
const settingsFormRef = ref<QForm>(); // for programtically verifying settings form

// Form validators and configurations
const invalidPrivateKeyMsg = 'Please enter a valid private key starting with 0x';
const isValidPrivateKey = (val: string) => !val || (isHexString(val) && val.length === 66) || invalidPrivateKeyMsg;
const isValidStartBlock = (val: string) => !val || Number(val) > 0 || 'Please enter a valid start block';
const isValidEndBlock = (val: string) => !val || Number(val) > 0 || 'Please enter a valid start block';
const setFormStatus = (scanStatusVal: ScanStatus, scanPrivateKey: string) => {
scanStatus.value = scanStatusVal;
setScanPrivateKey(scanPrivateKey);
scanPrivateKeyLocal.value = scanPrivateKey;
};

onMounted(async () => {
// If user already signed and we have their keys in memory, start scanning
if (hasKeys.value) {
await scan();
}
startBlockLocal.value = startBlock.value; // read in last used startBlock
endBlockLocal.value = endBlock.value; // read in last used endBlock
if (hasKeys.value) await scan(); // if user already signed and we have their keys in memory, start scanning
});

async function getPrivateKeysHandler() {
const success = await getPrivateKeys();
if (!success) return; // user denied signature or an error was thrown
await scan(); // start scanning right after we get the user's signature
// Validate form
if (advancedMode.value) {
const isFormValid = await settingsFormRef.value?.validate(true);
if (!isFormValid) return;
}
if (Number(startBlock.value) > Number(endBlock.value)) {
throw new Error('End block is larger than start block');
}

// Get user's signature if required
if (needsSignature.value) {
const success = await getPrivateKeys();
if (!success) return; // user denied signature or an error was thrown
}

// Save off the scanPrivateKeyLocal to memory if it exists, then scan
if (scanPrivateKeyLocal.value) setScanPrivateKey(scanPrivateKeyLocal.value);
await scan();
}

async function scan() {
if (!umbra.value) {
throw new Error('No umbra instance found. Please make sure you are on a supported network');
}
if (!umbra.value) throw new Error('No umbra instance found. Please make sure you are on a supported network');
scanStatus.value = 'scanning';
const { userAnnouncements: announcements } = await umbra.value.scan(
String(spendingKeyPair.value?.publicKeyHex),
String(viewingKeyPair.value?.privateKeyHex)
);

// Check for manually entered private key in advancedMode, otherwise use the key from user's signature
const chooseKey = (keyPair: string | undefined | null) => {
if (advancedMode.value && scanPrivateKeyLocal.value) return String(scanPrivateKeyLocal.value);
return String(keyPair);
};

// Scan for funds
const spendingPubKey = chooseKey(spendingKeyPair.value?.publicKeyHex);
const viewingPrivKey = chooseKey(viewingKeyPair.value?.privateKeyHex);
const overrides = { startBlock: startBlock.value, endBlock: endBlock.value };
const { userAnnouncements: announcements } = await umbra.value.scan(spendingPubKey, viewingPrivKey, overrides);
userAnnouncements.value = announcements;
scanStatus.value = 'complete';
}

return { userAddress, hasKeys, scanStatus, getPrivateKeysHandler, userAnnouncements };
return {
advancedMode,
endBlockLocal,
getPrivateKeysHandler,
isValidEndBlock,
isValidPrivateKey,
isValidStartBlock,
needsSignature,
scanPrivateKeyLocal,
scanStatus,
setFormStatus,
setScanBlocks,
settingsFormRef,
startBlockLocal,
userAddress,
userAnnouncements,
};
}

export default defineComponent({
Expand Down
Loading