Skip to content

Commit

Permalink
Merge pull request #53 from daniel/advanced-payment-info-line
Browse files Browse the repository at this point in the history
Advanced payment info line
  • Loading branch information
danimoh committed Jan 28, 2020
2 parents 35ba958 + c717ebc commit 908ee84
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 92 deletions.
16 changes: 10 additions & 6 deletions src/components/Account.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" stroke="white" stroke-linecap="round" stroke-width="2.5"><path d="M40.25 23.25v-.5a6.5 6.5 0 0 0-6.5-6.5h-3.5a6.5 6.5 0 0 0-6.5 6.5v6.5a6.5 6.5 0 0 0 6.5 6.5h2"/><path d="M23.75 40.75v.5a6.5 6.5 0 0 0 6.5 6.5h3.5a6.5 6.5 0 0 0 6.5-6.5v-6.5a6.5 6.5 0 0 0-6.5-6.5h-2"/><path d="M32 11.25v4M32 48.75v4"/></svg>
</div>
</div>
<Identicon v-else :address="address"/>
<Identicon v-else-if="_isNimiqAddress" :address="address"/>

<div v-if="!editable" class="label" :class="{ 'address-font': _isLabelAddress }">{{ label }}</div>
<div v-else class="label editable" :class="{ 'address-font': _isLabelAddress }">
<div v-if="!editable" class="label" :class="{ 'address-font': _isLabelNimiqAddress }">{{ label }}</div>
<div v-else class="label editable" :class="{ 'address-font': _isLabelNimiqAddress }">
<LabelInput :maxBytes="63" :value="label" :placeholder="placeholder" @input="changed" ref="label"/>
</div>

Expand All @@ -33,10 +33,10 @@
@Component({components: {Amount, Identicon, LabelInput}})
export default class Account extends Vue {
@Prop(String) public address!: string;
@Prop(String) public label!: string;
@Prop(String) public address?: string;
@Prop(String) public image?: string;
@Prop({type: Boolean, default: false}) public displayAsCashlink!: boolean;
@Prop(String) public label!: string;
@Prop(String) public placeholder?: string;
@Prop(String) public walletLabel?: string;
@Prop(Number) public balance?: number;
Expand Down Expand Up @@ -65,7 +65,11 @@
this.showImage = !!this.image;
}
private get _isLabelAddress() {
private get _isNimiqAddress() {
return ValidationUtils.isValidAddress(this.address);
}
private get _isLabelNimiqAddress() {
return ValidationUtils.isValidAddress(this.label);
}
}
Expand Down
194 changes: 116 additions & 78 deletions src/components/PaymentInfoLine.vue
Original file line number Diff line number Diff line change
@@ -1,41 +1,97 @@
<template>
<div class="info-line">
<Amount :amount="amount + fee" :decimals="decimals" />
<div class="info-line" :class="{ 'inverse-theme': theme === constructor.Themes.INVERSE }">
<div class="amounts">
<Amount
:currency="cryptoAmount.currency"
:amount="cryptoAmount.amount"
:totalDecimals="cryptoAmount.decimals"
:minDecimals="0"
:maxDecimals="Math.min(4, cryptoAmount.decimals)"
/>
<FiatAmount v-if="fiatAmount" class="fiat-amount"
:currency="fiatAmount.currency"
:amount="fiatAmount.amount"
/>
</div>
<div class="arrow-runway">
<ArrowRightSmallIcon/>
</div>
<a href="javascript:void(0)" class="description" @click="merchantInfoClicked">
<Account :address="address" :image="shopLogoUrl" :label="originDomain" />
<div class="info-circle-container">
<InfoCircleIcon class="info-circle"/>
</div>
</a>
<Account :address="address" :image="shopLogoUrl" :label="originDomain" />
<Timer v-if="startTime && endTime" ref="timer" :startTime="startTime" :endTime="endTime" :theme="theme" />
</div>
</template>

<script lang="ts">
import {Component, Prop, Emit, Vue} from 'vue-property-decorator';
import Amount from './Amount.vue';
// this imports only the type without bundling the library
type BigInteger = import ('big-integer').BigInteger;
import { Component, Prop, Vue } from 'vue-property-decorator';
import Account from './Account.vue';
import { InfoCircleIcon, ArrowRightSmallIcon } from './Icons';
import Timer from './Timer.vue';
import Amount from './Amount.vue';
import FiatAmount from './FiatAmount.vue';
import { ArrowRightSmallIcon } from './Icons';
interface CryptoAmountInfo {
amount: number | bigint | BigInteger; // in the smallest unit
currency: string;
decimals: number;
}
@Component({components: {Amount, Account, InfoCircleIcon, ArrowRightSmallIcon}})
export default class PaymentInfoLine extends Vue {
interface FiatAmountInfo {
amount: number; // in the base unit, e.g. Euro
currency: string;
}
function cryptoAmountInfoValidator(value: any) {
return 'amount' in value && 'currency' in value && 'decimals' in value
&& (typeof value.amount === 'number' || typeof value.amount === 'bigint'
|| (value.amount && value.amount.constructor && value.amount.constructor.name.endsWith('Integer')))
&& typeof value.currency === 'string'
&& typeof value.decimals === 'number' && Number.isInteger(value.decimals);
}
function fiatAmountInfoValidator(value: any) {
return 'amount' in value && 'currency' in value
&& typeof value.amount === 'number'
&& typeof value.currency === 'string';
}
@Component({components: {Account, Timer, Amount, FiatAmount, ArrowRightSmallIcon}})
class PaymentInfoLine extends Vue {
private get originDomain() {
return this.origin.split('://')[1];
}
@Prop({type: Number, default: 2}) public decimals!: number;
@Prop(Number) private amount!: number;
@Prop(Number) private fee!: number;
@Prop(String) private origin!: string;
@Prop(String) private address?: string;
@Prop(String) private shopLogoUrl?: string;
@Emit()
// tslint:disable-next-line no-empty
private merchantInfoClicked() {}
@Prop({type: Object, required: true, validator: cryptoAmountInfoValidator}) public cryptoAmount!: CryptoAmountInfo;
@Prop({type: Object, validator: fiatAmountInfoValidator}) public fiatAmount?: FiatAmountInfo;
@Prop({type: String, required: true}) public origin!: string;
@Prop(String) public address?: string;
@Prop(String) public shopLogoUrl?: string;
@Prop(Number) public startTime?: number;
@Prop(Number) public endTime?: number;
@Prop({
type: String,
validator: (value: any) => Object.values(PaymentInfoLine.Themes).includes(value),
default: 'normal',
})
public theme!: string;
public async setTime(time: number) {
await this.$nextTick(); // let vue update in case the timer was just added
if (!this.$refs.timer) return;
(this.$refs.timer as Timer).synchronize(time);
}
}
namespace PaymentInfoLine { // tslint:disable-line no-namespace
export enum Themes {
NORMAL = 'normal',
INVERSE = 'inverse',
}
}
export default PaymentInfoLine;
</script>

<style scoped>
Expand All @@ -44,20 +100,41 @@ export default class PaymentInfoLine extends Vue {
flex-direction: row;
justify-content: space-between;
box-sizing: border-box;
margin: 2rem 2.5rem 1rem 2.5rem;
margin: 1.75rem 2.5rem 1rem 2.375rem;
flex-shrink: 0;
font-size: 2rem;
line-height: 1.5;
font-weight: normal;
}
.amounts {
display: flex;
flex-direction: column;
justify-content: center;
margin-bottom: .125rem;
}
.amount {
font-weight: bold;
color: var(--nimiq-light-blue);
font-weight: bold;
}
.inverse-theme .amount {
color: var(--nimiq-light-blue-on-dark, var(--nimiq-light-blue));
}
.fiat-amount {
margin-top: .25rem;
color: var(--nimiq-blue);
font-size: 1.625rem;
line-height: 1;
font-weight: 600;
opacity: .6;
}
.arrow-runway {
flex-grow: 1;
min-width: 3rem;
display: flex;
flex-direction: row;
align-items: center;
Expand All @@ -80,31 +157,16 @@ export default class PaymentInfoLine extends Vue {
100% { transform: translate3D(3rem, 0, 0); }
}
.description {
display: flex;
flex-direction: row;
align-items: center;
color: inherit;
text-decoration: none;
outline: none;
}
.description:hover,
.description:focus {
text-decoration: none;
}
.account {
padding: 0;
width: auto !important;
flex-shrink: 1;
}
.account >>> .identicon {
min-width: unset;
width: 3.375rem;
height: 3.375rem;
margin-top: -0.25rem;
margin-bottom: -0.125rem;
margin-right: 0;
}
Expand All @@ -117,48 +179,24 @@ export default class PaymentInfoLine extends Vue {
}
.account >>> .label {
padding-left: 0;
transition: opacity .3s ease;
padding-left: .75rem;
margin-bottom: .25rem;
font-weight: unset;
opacity: 1;
mask-image: unset; /* Remove gradient-fade-out */
opacity: 1 !important;
/* Remove gradient-fade-out and use text-overflow instead */
mask-image: unset;
white-space: nowrap;
text-overflow: fade;
}
.info-circle-container {
position: relative;
opacity: 0.3;
margin-left: 1rem;
transition: opacity .3s ease;
}
.info-circle-container .nq-icon {
display: block;
}
.description:hover .account >>> .label,
.description:focus .account >>> .label {
opacity: .7;
}
.description:hover .info-circle-container,
.description:focus .info-circle-container {
opacity: 1;
}
.info-circle-container::after {
content: "";
position: absolute;
left: -0.625rem;
top: -0.625rem;
right: -0.625rem;
bottom: -0.625rem;
border: 0.25rem solid rgba(5, 130, 202, 0.5); /* Based on Nimiq Light Blue */
border-radius: 50%;
opacity: 0;
.timer {
margin: auto -.5rem auto 1rem;
flex-shrink: 0;
}
.description:focus .info-circle-container::after {
opacity: 1;
.inverse-theme .fiat-amount,
.inverse-theme .arrow-runway .nq-icon,
.inverse-theme .account >>> .label {
color: white;
}
</style>
30 changes: 22 additions & 8 deletions src/stories/index.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -941,18 +941,32 @@ storiesOf('Components', module)
};
})
.add('PaymentInfoLine', () => {
const address = text('address', 'NQ07 0000 00000000 0000 0000 0000 0000 0000');
const theme = select('theme', Object.values(PaymentInfoLine.Themes), PaymentInfoLine.Themes.NORMAL);
const cryptoAmount = {
amount: number('cryptoAmount.amount', 199862),
currency: text('cryptoAmount.currency', 'NIM'),
decimals: number('cryptoAmount.decimals', 5),
};
let fiatAmount = {
amount: number('fiatAmount.amount (-1 for unset)', -1),
currency: text('fiatAmount.currency', 'EUR'),
};
if (fiatAmount.amount < 0) fiatAmount = null;
const origin = text('origin', 'https://shop.nimiq.com');
const address = text('address', 'NQ07 0000 00000000 0000 0000 0000 0000 0000');
const shopLogo = text('shopLogo', 'https://pbs.twimg.com/profile_images/1150268408287698945/x4f3ITmx_400x400.png');
const amount = number('amount', 199862);
const fee = number('fee', 138);
let startTime = number('startTime', Date.now());
let expires = number('expires (-1 for unset)', -1);
if (expires < 0) expires = null;

return {
components: {PaymentInfoLine},
methods: {
merchantInfoClicked: action('merchant-info-clicked'),
},
template: `<div style="width: 400px"><PaymentInfoLine address="${address}" :amount="${amount}" :fee="${fee}"
origin="${origin}" shopLogoUrl="${shopLogo}" @merchant-info-clicked="merchantInfoClicked"/></div>`,
data: () => ({ cryptoAmount, fiatAmount, origin, address, shopLogo, startTime, expires, theme }),
template: `<div style="max-width: 420px" :class="{ 'nq-blue-bg': theme === 'inverse' }">
<PaymentInfoLine :cryptoAmount="cryptoAmount" :fiatAmount="fiatAmount"
:origin="origin" :address="address" :shopLogoUrl="shopLogo" :startTime="startTime" :expires="expires"
:theme="theme"/>
</div>`,
};
})
.add('QrCode', () => {
Expand Down

0 comments on commit 908ee84

Please sign in to comment.