Merge pull request #1579 from gradido/frontend-generate-link-for-send-gdd

Frontend generate link for send gdd
This commit is contained in:
Alexander Friedland 2022-03-14 22:49:34 +01:00 committed by GitHub
commit 84f9aaeb96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 926 additions and 289 deletions

View File

@ -94,6 +94,11 @@
border-color: #5e72e4;
}
.gradido-font-large {
font-size: large;
height: auto !important;
}
a,
.copyright {
color: #5a7b02;
@ -201,10 +206,6 @@ a,
background-color: #fff;
}
.gradido-font-large {
font-size: large;
}
.gradido-font-15rem {
font-size: 1.5rem;
}

View File

@ -0,0 +1,37 @@
<template>
<div class="clipboard-copy">
<b-input-group size="lg" class="mb-3" prepend="Link">
<b-form-input v-model="url" type="text" readonly></b-form-input>
<b-input-group-append>
<b-button size="sm" text="Button" variant="success" @click="CopyLink">
{{ $t('gdd_per_link.copy') }}
</b-button>
</b-input-group-append>
</b-input-group>
</div>
</template>
<script>
export default {
name: 'ClipboardCopy',
props: {
code: { type: String, required: true },
},
data() {
return {
url: `${window.location.origin}/redeem/${this.code}`,
}
},
methods: {
CopyLink() {
navigator.clipboard
.writeText(this.url)
.then(() => {
this.toastSuccess(this.$t('gdd_per_link.link-copied'))
})
.catch(() => {
this.toastError(this.$t('gdd_per_link.not-copied'))
})
},
},
}
</script>

View File

@ -1,18 +1,26 @@
<template>
<div class="gdd-send">
<slot :name="transactionSteps[currentTransactionStep]" />
<slot :name="currentTransactionStep" />
</div>
</template>
<script>
export const TRANSACTION_STEPS = {
transactionForm: 'transactionForm',
transactionConfirmationSend: 'transactionConfirmationSend',
transactionConfirmationLink: 'transactionConfirmationLink',
transactionResultSendSuccess: 'transactionResultSendSuccess',
transactionResultSendError: 'transactionResultSendError',
transactionResultLink: 'transactionResultLink',
}
export default {
name: 'GddSend',
data() {
return {
transactionSteps: ['transaction-form', 'transaction-confirmation', 'transaction-result'],
}
},
props: {
currentTransactionStep: { type: Number, default: 0 },
currentTransactionStep: {
type: String,
default: TRANSACTION_STEPS.transactionForm,
validator: (transactionStep) => TRANSACTION_STEPS[transactionStep] !== undefined,
},
},
}
</script>

View File

@ -0,0 +1,50 @@
import { mount } from '@vue/test-utils'
import TransactionConfirmationLink from './TransactionConfirmationLink'
const localVue = global.localVue
describe('GddSend confirm', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$i18n: {
locale: jest.fn(() => 'en'),
},
}
const propsData = {
balance: 1234,
email: 'user@example.org',
amount: 12.34,
memo: 'Pessimisten stehen im Regen, Optimisten duschen unter den Wolken.',
loading: false,
selected: 'send',
}
const Wrapper = () => {
return mount(TransactionConfirmationLink, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component div.transaction-confirm-link', () => {
expect(wrapper.find('div.transaction-confirm-link').exists()).toBeTruthy()
})
describe('has selected "link"', () => {
beforeEach(async () => {
await wrapper.setProps({
selected: 'link',
})
})
it('renders the component div.confirm-box-link', () => {
expect(wrapper.findAll('div.confirm-box-link').at(0).exists()).toBeTruthy()
})
})
})
})

View File

@ -0,0 +1,73 @@
<template>
<div class="transaction-confirm-link">
<b-row class="confirm-box-link">
<b-col class="text-right mt-4 mb-3">
<div class="alert-heading text-left h3">{{ $t('gdd_per_link.header') }}</div>
<h1>{{ (amount * -1) | GDD }}</h1>
<b class="mt-2">{{ memo }}</b>
</b-col>
</b-row>
<b-container class="bv-example-row mt-3 mb-3 gray-background p-2">
<div class="alert-heading text-left h3">{{ $t('advanced-calculation') }}</div>
<b-row class="pr-3">
<b-col class="text-right">{{ $t('form.current_balance') }}</b-col>
<b-col class="text-right">{{ balance | GDD }}</b-col>
</b-row>
<b-row class="pr-3">
<b-col class="text-right">
<strong>{{ $t('form.your_amount') }}</strong>
</b-col>
<b-col class="text-right">
<strong>{{ (amount * -1) | GDD }}</strong>
</b-col>
</b-row>
<b-row class="pr-3">
<b-col class="text-right">
<strong>{{ $t('gdd_per_link.decay-14-day') }}</strong>
</b-col>
<b-col class="text-right borderbottom">
<strong>~ {{ (amount * 0.028 * -1) | GDD }}</strong>
</b-col>
</b-row>
<b-row class="pr-3">
<b-col class="text-right">{{ $t('form.new_balance') }}</b-col>
<b-col class="text-right">~ {{ (balance - amount - amount * 0.028) | GDD }}</b-col>
</b-row>
</b-container>
<b-row class="mt-4">
<b-col>
<b-button @click="$emit('on-reset')">{{ $t('form.cancel') }}</b-button>
</b-col>
<b-col class="text-right">
<b-button variant="success" :disabled="loading" @click="$emit('send-transaction')">
{{ $t('form.generate_now') }}
</b-button>
</b-col>
</b-row>
</div>
</template>
<script>
export default {
name: 'TransactionConfirmationLink',
props: {
balance: { type: Number, required: true },
email: { type: String, required: false, default: '' },
amount: { type: Number, required: true },
memo: { type: String, required: true },
loading: { type: Boolean, required: true },
selected: { type: String, required: true },
},
}
</script>
<style>
.gray-background {
background-color: #ecebe6a3 !important;
}
.borderbottom {
border-bottom: 1px solid rgb(70, 65, 65);
border-bottom-style: double;
}
</style>

View File

@ -0,0 +1,50 @@
import { mount } from '@vue/test-utils'
import TransactionConfirmationSend from './TransactionConfirmationSend'
const localVue = global.localVue
describe('GddSend confirm', () => {
let wrapper
const mocks = {
$t: jest.fn((t) => t),
$i18n: {
locale: jest.fn(() => 'en'),
},
}
const propsData = {
balance: 1234,
email: 'user@example.org',
amount: 12.34,
memo: 'Pessimisten stehen im Regen, Optimisten duschen unter den Wolken.',
loading: false,
selected: 'send',
}
const Wrapper = () => {
return mount(TransactionConfirmationSend, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component div.transaction-confirm-send', () => {
expect(wrapper.find('div.transaction-confirm-send').exists()).toBeTruthy()
})
describe('has selected "send"', () => {
beforeEach(async () => {
await wrapper.setProps({
selected: 'send',
})
})
it('renders the component div.confirm-box-send', () => {
expect(wrapper.find('div.confirm-box-send').exists()).toBeTruthy()
})
})
})
})

View File

@ -1,6 +1,6 @@
<template>
<div>
<b-row>
<div class="transaction-confirm-send">
<b-row class="confirm-box-send">
<b-col>
<div class="display-4 pb-4">{{ $t('form.send_check') }}</div>
<b-list-group class="">
@ -18,7 +18,7 @@
<div class="m-1 mt-2">GDD</div>
</b-input-group-prepend>
<div class="p-3">{{ $n(amount, 'decimal') }}</div>
<div class="p-3">{{ (amount * -1) | GDD }}</div>
</b-input-group>
<br />
@ -33,23 +33,23 @@
</b-col>
</b-row>
<b-container class="bv-example-row mt-3 gray-background p-2">
<p>{{ $t('advanced-calculation') }}</p>
<b-container class="bv-example-row mt-3 mb-3 gray-background p-2">
<div class="alert-heading text-left h3">{{ $t('advanced-calculation') }}</div>
<b-row class="pr-3">
<b-col class="text-right">{{ $t('form.current_balance') }}</b-col>
<b-col class="text-right">{{ $n(balance, 'decimal') }}</b-col>
<b-col class="text-right">{{ balance | GDD }}</b-col>
</b-row>
<b-row class="pr-3">
<b-col class="text-right">
<strong>{{ $t('form.your_amount') }}</strong>
</b-col>
<b-col class="text-right borderbottom">
<strong>- {{ $n(amount, 'decimal') }}</strong>
<strong>{{ (amount * -1) | GDD }}</strong>
</b-col>
</b-row>
<b-row class="pr-3">
<b-col class="text-right">{{ $t('form.new_balance') }}</b-col>
<b-col class="text-right">~ {{ $n(balance - amount, 'decimal') }}</b-col>
<b-col class="text-right">{{ (balance - amount) | GDD }}</b-col>
</b-row>
</b-container>
@ -67,21 +67,14 @@
</template>
<script>
export default {
name: 'TransactionConfirmation',
name: 'TransactionConfirmationSend',
props: {
balance: { type: Number, default: 0 },
email: { type: String, default: '' },
amount: { type: Number, default: 0 },
memo: { type: String, default: '' },
loading: { type: Boolean, default: false },
transactions: {
default: () => [],
},
},
data() {
return {
decay: this.transactions[0].balance,
}
balance: { type: Number, required: true },
email: { type: String, required: false, default: '' },
amount: { type: Number, required: true },
memo: { type: String, required: true },
loading: { type: Boolean, required: true },
selected: { type: String, required: true },
},
}
</script>

View File

@ -55,13 +55,21 @@ describe('GddSend', () => {
})
})
describe('is selected: "send"', () => {
beforeEach(async () => {
// await wrapper.setData({
// selected: 'send',
// })
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
})
describe('transaction form', () => {
beforeEach(() => {
wrapper.setProps({ balance: 100.0 })
})
describe('transaction form show because balance 100,0 GDD', () => {
it('has no warning message ', () => {
expect(wrapper.find('.text-danger').exists()).toBeFalsy()
expect(wrapper.find('.errors').exists()).toBeFalsy()
})
it('has a reset button', () => {
expect(wrapper.find('.test-buttons').findAll('button').at(0).attributes('type')).toBe(
@ -225,6 +233,7 @@ describe('GddSend', () => {
email: 'someone@watches.tv',
amount: 87.23,
memo: 'Long enough',
selected: 'send',
},
],
])
@ -232,4 +241,18 @@ describe('GddSend', () => {
})
})
})
describe('is selected: "link"', () => {
beforeEach(async () => {
// await wrapper.setData({
// selected: 'link',
// })
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
})
it('has no input field of id input-group-1', () => {
expect(wrapper.find('#input-group-1').isVisible()).toBeFalsy()
})
})
})
})

View File

@ -2,26 +2,39 @@
<b-row class="transaction-form">
<b-col xl="12" md="12" class="p-0">
<b-card class="p-0 m-0 gradido-custom-background">
<!-- -<QrCode @set-transaction="setTransaction"></QrCode> -->
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)" @reset="onReset">
<!-- <div>
<qrcode-drop-zone id="input-0" v-model="form.img"></qrcode-drop-zone>
<b-row>
<b-col>
<b-form-radio v-model="selected" name="radios" :value="sendTypes.send" size="lg">
{{ $t('send_gdd') }}
</b-form-radio>
</b-col>
<b-col>
<b-form-radio v-model="selected" name="radios" :value="sendTypes.link" size="lg">
{{ $t('send_per_link') }}
</b-form-radio>
</b-col>
</b-row>
<div class="mt-4" v-show="selected === sendTypes.link">
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2>
<div>
{{ $t('gdd_per_link.sentence_1') }}
</div>
</div>
<br />
-->
<div>
<validation-provider
v-show="selected === sendTypes.send"
name="Email"
:rules="{
required: true,
required: selected === sendTypes.send ? true : false,
email: true,
is_not: $store.state.email,
}"
v-slot="{ errors }"
>
<label class="input-1" for="input-1">{{ $t('form.recipient') }}</label>
<label class="input-1 mt-4" for="input-1">{{ $t('form.recipient') }}</label>
<b-input-group
id="input-group-1"
class="border border-default"
@ -112,7 +125,6 @@
</b-col>
</validation-provider>
</div>
<br />
<div v-if="!!isBalanceDisabled" class="text-danger">
{{ $t('form.no_gdd_available') }}
@ -125,7 +137,7 @@
</b-col>
<b-col class="text-right">
<b-button type="submit" variant="success">
{{ $t('form.send_now') }}
{{ selected === sendTypes.send ? $t('form.send_now') : $t('form.generate_now') }}
</b-button>
</b-col>
</b-row>
@ -138,16 +150,13 @@
</b-row>
</template>
<script>
// import QrCode from './QrCode'
// import { QrcodeDropZone } from 'vue-qrcode-reader'
import { BIcon } from 'bootstrap-vue'
import { SEND_TYPES } from '@/pages/Send.vue'
export default {
name: 'TransactionForm',
components: {
BIcon,
// QrCode,
// QrcodeDropZone,
},
props: {
balance: { type: Number, default: 0 },
@ -162,12 +171,14 @@ export default {
memo: '',
amountValue: 0.0,
},
selected: SEND_TYPES.send,
}
},
methods: {
onSubmit() {
this.normalizeAmount(true)
this.$emit('set-transaction', {
selected: this.selected,
email: this.form.email,
amount: this.form.amountValue,
memo: this.form.memo,
@ -179,11 +190,6 @@ export default {
this.form.amount = ''
this.form.memo = ''
},
/*
setTransaction(data) {
this.form.email = data.email
this.form.amount = data.amount
}, */
normalizeAmount(isValid) {
this.amountFocused = false
if (!isValid) return
@ -199,6 +205,9 @@ export default {
isBalanceDisabled() {
return this.balance <= 0 ? 'disabled' : false
},
sendTypes() {
return SEND_TYPES
},
},
}
</script>

View File

@ -0,0 +1,33 @@
<template>
<b-container>
<b-row>
<b-col>
<b-card class="p-0 gradido-custom-background">
<div class="p-4">
<div class="h3 mb-5">{{ $t('gdd_per_link.created') }}</div>
<clipboard-copy :code="code" />
</div>
<p class="text-center mt-3">
<b-button variant="success" @click="$emit('on-reset')">{{ $t('form.close') }}</b-button>
</p>
</b-card>
</b-col>
</b-row>
</b-container>
</template>
<script>
import ClipboardCopy from '../ClipboardCopy.vue'
export default {
name: 'TransactionResultLink',
components: {
ClipboardCopy,
},
props: {
code: {
type: String,
required: true,
},
},
}
</script>

View File

@ -1,6 +1,6 @@
<template>
<b-container>
<b-row v-if="error">
<b-row>
<b-col>
<b-card class="p-0 gradido-custom-background">
<div class="p-4 gradido-font-15rem">
@ -27,25 +27,11 @@
</b-card>
</b-col>
</b-row>
<b-row v-if="!error">
<b-col>
<b-card class="p-0 gradido-custom-background">
<div class="p-4">
{{ $t('form.thx') }}
<hr />
{{ $t('form.send_transaction_success') }}
</div>
<p class="text-center mt-3">
<b-button variant="success" @click="$emit('on-reset')">{{ $t('form.close') }}</b-button>
</p>
</b-card>
</b-col>
</b-row>
</b-container>
</template>
<script>
export default {
name: 'TransactionResult',
name: 'TransactionResultSend',
props: {
error: { type: Boolean, default: false },
errorResult: { type: String, default: '' },

View File

@ -0,0 +1,23 @@
<template>
<b-container>
<b-row>
<b-col>
<b-card class="p-0 gradido-custom-background">
<div class="p-4">
{{ $t('form.thx') }}
<hr />
{{ $t('form.send_transaction_success') }}
</div>
<p class="text-center mt-3">
<b-button variant="success" @click="$emit('on-reset')">{{ $t('form.close') }}</b-button>
</p>
</b-card>
</b-col>
</b-row>
</b-container>
</template>
<script>
export default {
name: 'TransactionResultSend',
}
</script>

View File

@ -19,7 +19,7 @@ describe('GddTransactionList', () => {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$i18n: {
locale: () => 'de',
locale: () => 'en',
},
}
@ -96,13 +96,13 @@ describe('GddTransactionList', () => {
typeId: 'DECAY',
amount: '-0.16778637075575395772595',
balance: '31.59320453982945549519405',
balanceDate: '2022-03-03T08:54:54.020Z',
balanceDate: '2022-03-03T08:54:54',
memo: '',
linkedUser: null,
decay: {
decay: '-0.16778637075575395772595',
start: '2022-02-28T13:55:47.000Z',
end: '2022-03-03T08:54:54.020Z',
start: '2022-02-28T13:55:47',
end: '2022-03-03T08:54:54',
duration: 241147.02,
},
},
@ -111,7 +111,7 @@ describe('GddTransactionList', () => {
typeId: 'SEND',
amount: '1',
balance: '31.76099091058520945292',
balanceDate: '2022-02-28T13:55:47.000Z',
balanceDate: '2022-02-28T13:55:47',
memo: 'adasd adada',
linkedUser: {
firstName: 'Bibi',
@ -119,8 +119,8 @@ describe('GddTransactionList', () => {
},
decay: {
decay: '-0.2038314055482643084',
start: '2022-02-25T07:29:26.000Z',
end: '2022-02-28T13:55:47.000Z',
start: '2022-02-25T07:29:26',
end: '2022-02-28T13:55:47',
duration: 282381,
},
},
@ -129,7 +129,7 @@ describe('GddTransactionList', () => {
typeId: 'CREATION',
amount: '1000',
balance: '32.96482231613347376132',
balanceDate: '2022-02-25T07:29:26.000Z',
balanceDate: '2022-02-25T07:29:26',
memo: 'asd adada dad',
linkedUser: {
firstName: 'Gradido',
@ -137,8 +137,8 @@ describe('GddTransactionList', () => {
},
decay: {
decay: '-0.03517768386652623868',
start: '2022-02-23T10:55:30.000Z',
end: '2022-02-25T07:29:26.000Z',
start: '2022-02-23T10:55:30',
end: '2022-02-25T07:29:26',
duration: 160436,
},
},
@ -147,7 +147,7 @@ describe('GddTransactionList', () => {
typeId: 'RECEIVE',
amount: '10',
balance: '10',
balanceDate: '2022-02-23T10:55:30.000Z',
balanceDate: '2022-02-23T10:55:30',
memo: 'asd adaaad adad addad ',
linkedUser: {
firstName: 'Bibi',
@ -256,7 +256,7 @@ describe('GddTransactionList', () => {
it('shows the date of the transaction', () => {
expect(transaction.findAll('.gdd-transaction-list-item-date').at(0).text()).toContain(
'Mon Feb 28 2022 13:55:47',
'Mon Feb 28 2022 13:55:47 GMT+0000',
)
})
@ -326,7 +326,7 @@ describe('GddTransactionList', () => {
it('shows the date of the transaction', () => {
expect(transaction.findAll('.gdd-transaction-list-item-date').at(0).text()).toContain(
'Fri Feb 25 2022 07:29:26',
'Fri Feb 25 2022 07:29:26 GMT+0000',
)
})
})
@ -388,7 +388,7 @@ describe('GddTransactionList', () => {
it('shows the date of the transaction', () => {
expect(transaction.findAll('.gdd-transaction-list-item-date').at(0).text()).toContain(
'Wed Feb 23 2022 10:55:30',
'Wed Feb 23 2022 10:55:30 GMT+0000',
)
})

View File

@ -61,3 +61,11 @@ export const sendCoins = gql`
sendCoins(email: $email, amount: $amount, memo: $memo)
}
`
export const createTransactionLink = gql`
mutation($amount: Decimal!, $memo: String!) {
createTransactionLink(amount: $amount, memo: $memo) {
code
}
}
`

View File

@ -127,3 +127,18 @@ export const communities = gql`
}
}
`
export const queryTransactionLink = gql`
query($code: String!) {
queryTransactionLink(code: $code) {
amount
memo
createdAt
validUntil
user {
firstName
publisherId
}
}
}
`

View File

@ -67,6 +67,7 @@
"email": "E-Mail",
"firstname": "Vorname",
"from": "Von",
"generate_now": "jetzt generieren",
"lastname": "Nachname",
"memo": "Nachricht",
"message": "Nachricht",
@ -99,6 +100,19 @@
},
"your_amount": "Dein Betrag"
},
"gdd_per_link": {
"copy": "kopieren",
"created": "Der Link wurde erstellt!",
"decay-14-day": "Vergänglichkeit für 14 Tage",
"header": "Gradidos versenden per Link",
"link-copied": "Link wurde in die Zwischenablage kopiert",
"not-copied": "Konnte den Link nicht kopieren: {err}",
"sentence_1": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „jetzt generieren“ wird ein Link erstellt, den du versenden kannst.",
"sentence_2": "Der Link ist 14 Tage gültig!",
"sentence_3": "Wird der Link nicht innerhalb der 14 Tage Frist abgerufen, wird er automatisch gelöscht. Der Betrag wird dann wieder deinem Konto gutgeschrieben.",
"sentence_4": "Wer den Link aktiviert, erhält die Zahlung von deinem Konto. Wer noch nicht Mitglied bei Gradido ist, wird durch den Registrierungsprozess geleitet und bekommt den GDD Betrag nach Registrierung / Bestätigung seines Gradido Kontos gut geschrieben.",
"sentence_5": "Der Vergänglichkeitsbetrag wird von dir getragen und für die maximale Gültigkeit deinem Betrag aufgerechnet. Dir wird aber nur der Betrag als Vergänglichkeit berechnet, je nachdem wie viel Tage bis zum Einlösen des Links vergangen sind. Bis zum Einlösen wird die Vergänglichkeit für den gesamten Zeitraum der Gültigkeit vorgehalten."
},
"gdt": {
"action": "Aktion",
"calculation": "Berechnung der GradidoTransform",
@ -129,6 +143,8 @@
"publisherId": "Publisher-Id"
},
"send": "Senden",
"send_gdd": "GDD versenden",
"send_per_link": "GDD versenden per Link",
"settings": {
"coinanimation": {
"coinanimation": "Münzanimation",
@ -221,6 +237,10 @@
"receiverNotFound": "Empfänger nicht gefunden",
"show_all": "Alle <strong>{count}</strong> Transaktionen ansehen"
},
"transaction-link": {
"button": "einlösen",
"send_you": "sendet dir"
},
"transactions": "Transaktionen",
"whitepaper": "Whitepaper"
}

View File

@ -67,6 +67,7 @@
"email": "Email",
"firstname": "Firstname",
"from": "from",
"generate_now": "Generate now",
"lastname": "Lastname",
"memo": "Message",
"message": "Message",
@ -99,6 +100,19 @@
},
"your_amount": "Your amount"
},
"gdd_per_link": {
"copy": "copy",
"created": "Link was created!",
"decay-14-day": "Decay for 14 days",
"header": "Send Gradidos via link",
"link-copied": "Link copied to clipboard",
"not-copied": "Could not copy link: {err}",
"sentence_1": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share.",
"sentence_2": "The link is valid for 14 days!",
"sentence_3": "If the link is not redeemed within the 14-day validity period, it will be invalidated automatically. The amount will then be available to your account again.",
"sentence_4": "Whoever activates the link will receive the payment from your account. If the recipient is not yet a member of Gradido, he will be guided through the registration process and will get the GDD amount credited after registration / confirmation of the newly created Gradido account.",
"sentence_5": "The decay amount will be blocked for your account for the whole duration of the link. However, you will only be charged the amount of decay for the time which has actually passed until the link is redeemed. The remaining amount will then be available to your account again."
},
"gdt": {
"action": "Action",
"calculation": "Calculation of GradidoTransform",
@ -129,6 +143,8 @@
"publisherId": "PublisherID"
},
"send": "Send",
"send_gdd": "GDD send",
"send_per_link": "GDD send via link",
"settings": {
"coinanimation": {
"coinanimation": "Coin animation",
@ -221,6 +237,10 @@
"receiverNotFound": "Recipient not found",
"show_all": "View all <strong>{count}</strong> transactions."
},
"transaction-link": {
"button": "redeem",
"send_you": "wants to send you"
},
"transactions": "Transactions",
"whitepaper": "Whitepaper"
}

View File

@ -13,7 +13,7 @@ export const toasters = {
})
},
toast(message, options) {
message = message.replace(/^GraphQL error: /, '')
if (message.replace) message = message.replace(/^GraphQL error: /, '')
this.$bvToast.toast(message, {
autoHideDelay: 5000,
appendToast: true,

View File

@ -1,13 +1,16 @@
import { mount } from '@vue/test-utils'
import Send from './Send'
import Send, { SEND_TYPES } from './Send'
import { toastErrorSpy, toastSuccessSpy } from '@test/testSetup'
import { TRANSACTION_STEPS } from '@/components/GddSend.vue'
import { sendCoins, createTransactionLink } from '@/graphql/mutations.js'
const sendMock = jest.fn()
sendMock.mockResolvedValue('success')
const apolloMutationMock = jest.fn()
apolloMutationMock.mockResolvedValue('success')
const navigatorClipboardMock = jest.fn()
const localVue = global.localVue
// window.scrollTo = jest.fn()
describe('Send', () => {
let wrapper
@ -16,6 +19,7 @@ describe('Send', () => {
GdtBalance: 1234.56,
transactions: [{ balance: 0.1 }],
pending: true,
currentTransactionStep: TRANSACTION_STEPS.transactionConfirmationSend,
}
const mocks = {
@ -27,7 +31,7 @@ describe('Send', () => {
},
},
$apollo: {
mutate: sendMock,
mutate: apolloMutationMock,
},
}
@ -44,38 +48,42 @@ describe('Send', () => {
expect(wrapper.find('div.gdd-send').exists()).toBeTruthy()
})
describe('transaction form', () => {
/* SEND */
describe('transaction form send', () => {
beforeEach(async () => {
wrapper.findComponent({ name: 'TransactionForm' }).vm.$emit('set-transaction', {
email: 'user@example.org',
amount: 23.45,
memo: 'Make the best of it!',
selected: SEND_TYPES.send,
})
})
it('steps forward in the dialog', () => {
expect(wrapper.findComponent({ name: 'TransactionConfirmation' }).exists()).toBe(true)
expect(wrapper.findComponent({ name: 'TransactionConfirmationSend' }).exists()).toBe(true)
})
})
describe('confirm transaction', () => {
describe('confirm transaction if selected: SEND_TYPES.send', () => {
beforeEach(() => {
wrapper.setData({
currentTransactionStep: 1,
currentTransactionStep: TRANSACTION_STEPS.transactionConfirmationSend,
transactionData: {
email: 'user@example.org',
amount: 23.45,
memo: 'Make the best of it!',
selected: SEND_TYPES.send,
},
})
})
it('resets the transaction process when on-reset is emitted', async () => {
await wrapper.findComponent({ name: 'TransactionConfirmation' }).vm.$emit('on-reset')
await wrapper.findComponent({ name: 'TransactionConfirmationSend' }).vm.$emit('on-reset')
expect(wrapper.findComponent({ name: 'TransactionForm' }).exists()).toBeTruthy()
expect(wrapper.vm.transactionData).toEqual({
email: 'user@example.org',
amount: 23.45,
memo: 'Make the best of it!',
selected: SEND_TYPES.send,
})
})
@ -83,17 +91,19 @@ describe('Send', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper
.findComponent({ name: 'TransactionConfirmation' })
.findComponent({ name: 'TransactionConfirmationSend' })
.vm.$emit('send-transaction')
})
it('calls the API when send-transaction is emitted', async () => {
expect(sendMock).toBeCalledWith(
expect(apolloMutationMock).toBeCalledWith(
expect.objectContaining({
mutation: sendCoins,
variables: {
email: 'user@example.org',
amount: 23.45,
memo: 'Make the best of it!',
selected: SEND_TYPES.send,
},
}),
)
@ -104,7 +114,7 @@ describe('Send', () => {
expect(wrapper.emitted('update-balance')).toEqual([[23.45]])
})
it('shows the succes page', () => {
it('shows the success page', () => {
expect(wrapper.find('div.card-body').text()).toContain('form.send_transaction_success')
})
})
@ -112,9 +122,9 @@ describe('Send', () => {
describe('transaction is confirmed and server response is error', () => {
beforeEach(async () => {
jest.clearAllMocks()
sendMock.mockRejectedValue({ message: 'recipient not known' })
apolloMutationMock.mockRejectedValue({ message: 'recipient not known' })
await wrapper
.findComponent({ name: 'TransactionConfirmation' })
.findComponent({ name: 'TransactionConfirmationSend' })
.vm.$emit('send-transaction')
})
@ -131,5 +141,115 @@ describe('Send', () => {
})
})
})
/* LINK */
describe('transaction form link', () => {
beforeEach(async () => {
apolloMutationMock.mockResolvedValue({
data: { createTransactionLink: { code: '0123456789' } },
})
await wrapper.findComponent({ name: 'TransactionForm' }).vm.$emit('set-transaction', {
amount: 56.78,
memo: 'Make the best of it link!',
selected: SEND_TYPES.link,
})
})
it('steps forward in the dialog', () => {
expect(wrapper.findComponent({ name: 'TransactionConfirmationLink' }).exists()).toBe(true)
})
describe('transaction is confirmed and server response is success', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper
.findComponent({ name: 'TransactionConfirmationLink' })
.vm.$emit('send-transaction')
})
it('calls the API when send-transaction is emitted', async () => {
expect(apolloMutationMock).toBeCalledWith(
expect.objectContaining({
mutation: createTransactionLink,
variables: {
amount: 56.78,
memo: 'Make the best of it link!',
},
}),
)
})
it.skip('emits update-balance', () => {
expect(wrapper.emitted('update-balance')).toBeTruthy()
expect(wrapper.emitted('update-balance')).toEqual([[56.78]])
})
it('find components ClipBoard', () => {
expect(wrapper.findComponent({ name: 'ClipboardCopy' }).exists()).toBe(true)
})
it('shows the success message', () => {
expect(wrapper.find('div.card-body').text()).toContain('gdd_per_link.created')
})
it('shows the close button', () => {
expect(wrapper.find('div.card-body').text()).toContain('form.close')
})
describe('Copy link to Clipboard', () => {
const navigatorClipboard = navigator.clipboard
beforeAll(() => {
delete navigator.clipboard
navigator.clipboard = { writeText: navigatorClipboardMock }
})
afterAll(() => {
navigator.clipboard = navigatorClipboard
})
describe('copy with success', () => {
beforeEach(async () => {
navigatorClipboardMock.mockResolvedValue()
await wrapper.findAll('button').at(0).trigger('click')
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.link-copied')
})
})
describe('copy with error', () => {
beforeEach(async () => {
navigatorClipboardMock.mockRejectedValue()
await wrapper.findAll('button').at(0).trigger('click')
})
it('toasts error message', () => {
expect(toastErrorSpy).toBeCalledWith('gdd_per_link.not-copied')
})
})
})
describe('close button click', () => {
beforeEach(async () => {
await wrapper.findAll('button').at(1).trigger('click')
})
it('Shows the TransactionForm', () => {
expect(wrapper.findComponent({ name: 'TransactionForm' }).exists()).toBe(true)
})
})
})
describe('send apollo if transaction link with error', () => {
beforeEach(() => {
apolloMutationMock.mockRejectedValue({ message: 'OUCH!' })
wrapper.find('button.btn-success').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith({ message: 'OUCH!' })
})
})
})
})
})

View File

@ -2,27 +2,45 @@
<div>
<b-container>
<gdd-send :currentTransactionStep="currentTransactionStep" class="pt-3 ml-2 mr-2">
<template #transaction-form>
<template #transactionForm>
<transaction-form :balance="balance" @set-transaction="setTransaction"></transaction-form>
</template>
<template #transaction-confirmation>
<transaction-confirmation
<template #transactionConfirmationSend>
<transaction-confirmation-send
:balance="balance"
:transactions="transactions"
:selected="transactionData.selected"
:email="transactionData.email"
:amount="transactionData.amount"
:memo="transactionData.memo"
:loading="loading"
@send-transaction="sendTransaction"
@on-reset="onReset"
></transaction-confirmation>
></transaction-confirmation-send>
</template>
<template #transaction-result>
<transaction-result
<template #transactionConfirmationLink>
<transaction-confirmation-link
:balance="balance"
:selected="transactionData.selected"
:email="transactionData.email"
:amount="transactionData.amount"
:memo="transactionData.memo"
:loading="loading"
@send-transaction="sendTransaction"
@on-reset="onReset"
></transaction-confirmation-link>
</template>
<template #transactionResultSendSuccess>
<transaction-result-send-success @on-reset="onReset"></transaction-result-send-success>
</template>
<template #transactionResultSendError>
<transaction-result-send-error
:error="error"
:errorResult="errorResult"
@on-reset="onReset"
></transaction-result>
></transaction-result-send-error>
</template>
<template #transactionResultLink>
<transaction-result-link :code="code" @on-reset="onReset"></transaction-result-link>
</template>
</gdd-send>
<hr />
@ -30,11 +48,14 @@
</div>
</template>
<script>
import GddSend from '@/components/GddSend.vue'
import GddSend, { TRANSACTION_STEPS } from '@/components/GddSend.vue'
import TransactionForm from '@/components/GddSend/TransactionForm.vue'
import TransactionConfirmation from '@/components/GddSend/TransactionConfirmation.vue'
import TransactionResult from '@/components/GddSend/TransactionResult.vue'
import { sendCoins } from '@/graphql/mutations.js'
import TransactionConfirmationSend from '@/components/GddSend/TransactionConfirmationSend.vue'
import TransactionConfirmationLink from '@/components/GddSend/TransactionConfirmationLink.vue'
import TransactionResultSendSuccess from '@/components/GddSend/TransactionResultSendSuccess.vue'
import TransactionResultSendError from '@/components/GddSend/TransactionResultSendError.vue'
import TransactionResultLink from '@/components/GddSend/TransactionResultLink.vue'
import { sendCoins, createTransactionLink } from '@/graphql/mutations.js'
const EMPTY_TRANSACTION_DATA = {
email: '',
@ -42,21 +63,30 @@ const EMPTY_TRANSACTION_DATA = {
memo: '',
}
export const SEND_TYPES = {
send: 'send',
link: 'link',
}
export default {
name: 'Send',
components: {
GddSend,
TransactionForm,
TransactionConfirmation,
TransactionResult,
TransactionConfirmationSend,
TransactionConfirmationLink,
TransactionResultSendSuccess,
TransactionResultSendError,
TransactionResultLink,
},
data() {
return {
transactionData: { ...EMPTY_TRANSACTION_DATA },
error: false,
errorResult: '',
currentTransactionStep: 0,
currentTransactionStep: TRANSACTION_STEPS.transactionForm,
loading: false,
code: null,
}
},
props: {
@ -74,11 +104,20 @@ export default {
methods: {
setTransaction(data) {
this.transactionData = { ...data }
this.currentTransactionStep = 1
switch (data.selected) {
case SEND_TYPES.send:
this.currentTransactionStep = TRANSACTION_STEPS.transactionConfirmationSend
break
case SEND_TYPES.link:
this.currentTransactionStep = TRANSACTION_STEPS.transactionConfirmationLink
break
}
},
async sendTransaction() {
this.loading = true
this.error = false
switch (this.transactionData.selected) {
case SEND_TYPES.send:
this.$apollo
.mutate({
mutation: sendCoins,
@ -87,16 +126,35 @@ export default {
.then(() => {
this.error = false
this.$emit('update-balance', this.transactionData.amount)
this.currentTransactionStep = TRANSACTION_STEPS.transactionResultSendSuccess
})
.catch((err) => {
this.errorResult = err.message
this.error = true
this.currentTransactionStep = TRANSACTION_STEPS.transactionResultSendError
})
this.currentTransactionStep = 2
break
case SEND_TYPES.link:
this.$apollo
.mutate({
mutation: createTransactionLink,
variables: { amount: this.transactionData.amount, memo: this.transactionData.memo },
})
.then((result) => {
this.code = result.data.createTransactionLink.code
this.currentTransactionStep = TRANSACTION_STEPS.transactionResultLink
})
.catch((error) => {
this.toastError(error)
})
break
default:
throw new Error(`undefined transactionData.selected : ${this.transactionData.selected}`)
}
this.loading = false
},
onReset() {
this.currentTransactionStep = 0
this.currentTransactionStep = TRANSACTION_STEPS.transactionForm
},
updateTransactions(pagination) {
this.$emit('update-transactions', pagination)

View File

@ -0,0 +1,49 @@
import { mount } from '@vue/test-utils'
import ShowTransactionLinkInformations from './ShowTransactionLinkInformations'
const localVue = global.localVue
const errorHandler = jest.fn()
localVue.config.errorHandler = errorHandler
const queryTransactionLink = jest.fn()
queryTransactionLink.mockResolvedValue('success')
const createMockObject = (code) => {
return {
localVue,
mocks: {
$t: jest.fn((t) => t),
$i18n: {
locale: () => 'en',
},
$apollo: {
query: queryTransactionLink,
},
$route: {
params: {
code,
},
},
},
}
}
describe('ShowTransactionLinkInformations', () => {
let wrapper
const Wrapper = (functionN) => {
return mount(ShowTransactionLinkInformations, functionN)
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper(createMockObject())
})
it('renders the component', () => {
expect(wrapper.find('div.show-transaction-link-informations').exists()).toBeTruthy()
})
})
})

View File

@ -0,0 +1,57 @@
<template>
<div class="show-transaction-link-informations">
<!-- Header -->
<div class="header py-7 py-lg-8 pt-lg-9">
<b-container>
<div class="header-body text-center mb-7">
<p class="h1">
{{ displaySetup.user.firstName }}
{{ $t('transaction-link.send_you') }} {{ displaySetup.amount | GDD }}
</p>
<p class="h4">{{ displaySetup.memo }}</p>
<hr />
<b-button v-if="displaySetup.linkTo" :to="displaySetup.linkTo">
{{ $t('transaction-link.button') }}
</b-button>
</div>
</b-container>
</div>
</div>
</template>
<script>
import { queryTransactionLink } from '@/graphql/queries'
export default {
name: 'ShowTransactionLinkInformations',
data() {
return {
displaySetup: {
user: {
firstName: '',
},
},
}
},
methods: {
setTransactionLinkInformation() {
this.$apollo
.query({
query: queryTransactionLink,
variables: {
code: this.$route.params.code,
},
})
.then((result) => {
this.displaySetup = result.data.queryTransactionLink
this.$store.commit('publisherId', result.data.queryTransactionLink.user.publisherId)
})
.catch((error) => {
this.toastError(error)
})
},
},
created() {
this.setTransactionLinkInformation()
},
}
</script>

View File

@ -49,8 +49,8 @@ describe('router', () => {
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
})
it('has sixteen routes defined', () => {
expect(routes).toHaveLength(16)
it('has seventeen routes defined', () => {
expect(routes).toHaveLength(17)
})
describe('overview', () => {

View File

@ -82,6 +82,10 @@ const routes = [
path: '/checkEmail/:optin',
component: () => import('@/pages/ResetPassword.vue'),
},
{
path: '/redeem/:code',
component: () => import('@/pages/ShowTransactionLinkInformations.vue'),
},
{ path: '*', component: NotFound },
]