diff --git a/frontend/src/assets/scss/gradido.scss b/frontend/src/assets/scss/gradido.scss index abb491206..53a296713 100644 --- a/frontend/src/assets/scss/gradido.scss +++ b/frontend/src/assets/scss/gradido.scss @@ -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; } diff --git a/frontend/src/components/ClipboardCopy.vue b/frontend/src/components/ClipboardCopy.vue new file mode 100644 index 000000000..b952dbd8c --- /dev/null +++ b/frontend/src/components/ClipboardCopy.vue @@ -0,0 +1,37 @@ + + diff --git a/frontend/src/components/GddSend.vue b/frontend/src/components/GddSend.vue index cfab0899a..e0d7c6eba 100644 --- a/frontend/src/components/GddSend.vue +++ b/frontend/src/components/GddSend.vue @@ -1,18 +1,26 @@ diff --git a/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js b/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js new file mode 100644 index 000000000..a28c2d185 --- /dev/null +++ b/frontend/src/components/GddSend/TransactionConfirmationLink.spec.js @@ -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() + }) + }) + }) +}) diff --git a/frontend/src/components/GddSend/TransactionConfirmationLink.vue b/frontend/src/components/GddSend/TransactionConfirmationLink.vue new file mode 100644 index 000000000..18eb0f25d --- /dev/null +++ b/frontend/src/components/GddSend/TransactionConfirmationLink.vue @@ -0,0 +1,73 @@ + + + diff --git a/frontend/src/components/GddSend/TransactionConfirmationSend.spec.js b/frontend/src/components/GddSend/TransactionConfirmationSend.spec.js new file mode 100644 index 000000000..38dd82866 --- /dev/null +++ b/frontend/src/components/GddSend/TransactionConfirmationSend.spec.js @@ -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() + }) + }) + }) +}) diff --git a/frontend/src/components/GddSend/TransactionConfirmation.vue b/frontend/src/components/GddSend/TransactionConfirmationSend.vue similarity index 75% rename from frontend/src/components/GddSend/TransactionConfirmation.vue rename to frontend/src/components/GddSend/TransactionConfirmationSend.vue index 4acb95dcd..e05d42269 100644 --- a/frontend/src/components/GddSend/TransactionConfirmation.vue +++ b/frontend/src/components/GddSend/TransactionConfirmationSend.vue @@ -1,6 +1,6 @@ diff --git a/frontend/src/components/GddSend/TransactionForm.spec.js b/frontend/src/components/GddSend/TransactionForm.spec.js index 25683e6df..49b2174e0 100644 --- a/frontend/src/components/GddSend/TransactionForm.spec.js +++ b/frontend/src/components/GddSend/TransactionForm.spec.js @@ -55,180 +55,203 @@ describe('GddSend', () => { }) }) - 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() - }) - it('has a reset button', () => { - expect(wrapper.find('.test-buttons').findAll('button').at(0).attributes('type')).toBe( - 'reset', - ) - }) - it('has a submit button', () => { - expect(wrapper.find('.test-buttons').findAll('button').at(1).attributes('type')).toBe( - 'submit', - ) - }) + describe('is selected: "send"', () => { + beforeEach(async () => { + // await wrapper.setData({ + // selected: 'send', + // }) + await wrapper.findAll('input[type="radio"]').at(0).setChecked() }) - describe('email field', () => { - it('has an input field of type email', () => { - expect(wrapper.find('#input-group-1').find('input').attributes('type')).toBe('email') + 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('.errors').exists()).toBeFalsy() + }) + it('has a reset button', () => { + expect(wrapper.find('.test-buttons').findAll('button').at(0).attributes('type')).toBe( + 'reset', + ) + }) + it('has a submit button', () => { + expect(wrapper.find('.test-buttons').findAll('button').at(1).attributes('type')).toBe( + 'submit', + ) + }) }) - it('has an envelope icon', () => { - expect(wrapper.find('#input-group-1').find('svg').attributes('aria-label')).toBe( - 'envelope', - ) + describe('email field', () => { + it('has an input field of type email', () => { + expect(wrapper.find('#input-group-1').find('input').attributes('type')).toBe('email') + }) + + it('has an envelope icon', () => { + expect(wrapper.find('#input-group-1').find('svg').attributes('aria-label')).toBe( + 'envelope', + ) + }) + + it('has a label form.receiver', () => { + expect(wrapper.find('label.input-1').text()).toBe('form.recipient') + }) + + it('has a placeholder "E-Mail"', () => { + expect(wrapper.find('#input-group-1').find('input').attributes('placeholder')).toBe( + 'E-Mail', + ) + }) + + it('flushes an error message when no valid email is given', async () => { + await wrapper.find('#input-group-1').find('input').setValue('a') + await flushPromises() + expect(wrapper.find('span.errors').text()).toBe('validations.messages.email') + }) + + it('trims the email after blur', async () => { + await wrapper.find('#input-group-1').find('input').setValue(' valid@email.com ') + await wrapper.find('#input-group-1').find('input').trigger('blur') + await flushPromises() + expect(wrapper.vm.form.email).toBe('valid@email.com') + }) }) - it('has a label form.receiver', () => { - expect(wrapper.find('label.input-1').text()).toBe('form.recipient') + describe('amount field', () => { + it('has an input field of type text', () => { + expect(wrapper.find('#input-group-2').find('input').attributes('type')).toBe('text') + }) + + it('has an GDD text icon', () => { + expect(wrapper.find('#input-group-2').find('div.m-1').text()).toBe('GDD') + }) + + it('has a label form.amount', () => { + expect(wrapper.find('label.input-2').text()).toBe('form.amount') + }) + + it('has a placeholder "0.01"', () => { + expect(wrapper.find('#input-group-2').find('input').attributes('placeholder')).toBe( + '0.01', + ) + }) + + it('does not update form amount when invalid', async () => { + await wrapper.find('#input-group-2').find('input').setValue('invalid') + await wrapper.find('#input-group-2').find('input').trigger('blur') + await flushPromises() + expect(wrapper.vm.form.amountValue).toBe(0) + }) + + it('flushes an error message when no valid amount is given', async () => { + await wrapper.find('#input-group-2').find('input').setValue('a') + await flushPromises() + expect(wrapper.find('span.errors').text()).toBe('form.validation.gddSendAmount') + }) + + it('flushes an error message when amount is too high', async () => { + await wrapper.find('#input-group-2').find('input').setValue('123.34') + await flushPromises() + expect(wrapper.find('span.errors').text()).toBe('form.validation.gddSendAmount') + }) + + it('flushes no errors when amount is valid', async () => { + await wrapper.find('#input-group-2').find('input').setValue('87.34') + await flushPromises() + expect(wrapper.find('span.errors').exists()).toBeFalsy() + }) }) - it('has a placeholder "E-Mail"', () => { - expect(wrapper.find('#input-group-1').find('input').attributes('placeholder')).toBe( - 'E-Mail', - ) + describe('message text box', () => { + it('has an textarea field', () => { + expect(wrapper.find('#input-group-3').find('textarea').exists()).toBeTruthy() + }) + + it('has an chat-right-text icon', () => { + expect(wrapper.find('#input-group-3').find('svg').attributes('aria-label')).toBe( + 'chat right text', + ) + }) + + it('has a label form.message', () => { + expect(wrapper.find('label.input-3').text()).toBe('form.message') + }) + + it('flushes an error message when memo is less than 5 characters', async () => { + await wrapper.find('#input-group-3').find('textarea').setValue('a') + await flushPromises() + expect(wrapper.find('span.errors').text()).toBe('validations.messages.min') + }) + + it('flushes no error message when memo is valid', async () => { + await wrapper.find('#input-group-3').find('textarea').setValue('Long enough') + await flushPromises() + expect(wrapper.find('span.errors').exists()).toBeFalsy() + }) }) - it('flushes an error message when no valid email is given', async () => { - await wrapper.find('#input-group-1').find('input').setValue('a') - await flushPromises() - expect(wrapper.find('span.errors').text()).toBe('validations.messages.email') + describe('cancel button', () => { + it('has a cancel button', () => { + expect(wrapper.find('button[type="reset"]').exists()).toBeTruthy() + }) + + it('has the text "form.cancel"', () => { + expect(wrapper.find('button[type="reset"]').text()).toBe('form.reset') + }) + + it('clears all fields on click', async () => { + await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv') + await wrapper.find('#input-group-2').find('input').setValue('87.23') + await wrapper.find('#input-group-3').find('textarea').setValue('Long enough') + await flushPromises() + expect(wrapper.vm.form.email).toBe('someone@watches.tv') + expect(wrapper.vm.form.amount).toBe('87.23') + expect(wrapper.vm.form.memo).toBe('Long enough') + await wrapper.find('button[type="reset"]').trigger('click') + await flushPromises() + expect(wrapper.vm.form.email).toBe('') + expect(wrapper.vm.form.amount).toBe('') + expect(wrapper.vm.form.memo).toBe('') + }) }) - it('trims the email after blur', async () => { - await wrapper.find('#input-group-1').find('input').setValue(' valid@email.com ') - await wrapper.find('#input-group-1').find('input').trigger('blur') - await flushPromises() - expect(wrapper.vm.form.email).toBe('valid@email.com') + describe('submit', () => { + beforeEach(async () => { + await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv') + await wrapper.find('#input-group-2').find('input').setValue('87.23') + await wrapper.find('#input-group-3').find('textarea').setValue('Long enough') + await wrapper.find('form').trigger('submit') + await flushPromises() + }) + + it('emits set-transaction', async () => { + expect(wrapper.emitted('set-transaction')).toBeTruthy() + expect(wrapper.emitted('set-transaction')).toEqual([ + [ + { + email: 'someone@watches.tv', + amount: 87.23, + memo: 'Long enough', + selected: 'send', + }, + ], + ]) + }) }) }) + }) - describe('amount field', () => { - it('has an input field of type text', () => { - expect(wrapper.find('#input-group-2').find('input').attributes('type')).toBe('text') - }) - - it('has an GDD text icon', () => { - expect(wrapper.find('#input-group-2').find('div.m-1').text()).toBe('GDD') - }) - - it('has a label form.amount', () => { - expect(wrapper.find('label.input-2').text()).toBe('form.amount') - }) - - it('has a placeholder "0.01"', () => { - expect(wrapper.find('#input-group-2').find('input').attributes('placeholder')).toBe( - '0.01', - ) - }) - - it('does not update form amount when invalid', async () => { - await wrapper.find('#input-group-2').find('input').setValue('invalid') - await wrapper.find('#input-group-2').find('input').trigger('blur') - await flushPromises() - expect(wrapper.vm.form.amountValue).toBe(0) - }) - - it('flushes an error message when no valid amount is given', async () => { - await wrapper.find('#input-group-2').find('input').setValue('a') - await flushPromises() - expect(wrapper.find('span.errors').text()).toBe('form.validation.gddSendAmount') - }) - - it('flushes an error message when amount is too high', async () => { - await wrapper.find('#input-group-2').find('input').setValue('123.34') - await flushPromises() - expect(wrapper.find('span.errors').text()).toBe('form.validation.gddSendAmount') - }) - - it('flushes no errors when amount is valid', async () => { - await wrapper.find('#input-group-2').find('input').setValue('87.34') - await flushPromises() - expect(wrapper.find('span.errors').exists()).toBeFalsy() - }) + describe('is selected: "link"', () => { + beforeEach(async () => { + // await wrapper.setData({ + // selected: 'link', + // }) + await wrapper.findAll('input[type="radio"]').at(1).setChecked() }) - describe('message text box', () => { - it('has an textarea field', () => { - expect(wrapper.find('#input-group-3').find('textarea').exists()).toBeTruthy() - }) - - it('has an chat-right-text icon', () => { - expect(wrapper.find('#input-group-3').find('svg').attributes('aria-label')).toBe( - 'chat right text', - ) - }) - - it('has a label form.message', () => { - expect(wrapper.find('label.input-3').text()).toBe('form.message') - }) - - it('flushes an error message when memo is less than 5 characters', async () => { - await wrapper.find('#input-group-3').find('textarea').setValue('a') - await flushPromises() - expect(wrapper.find('span.errors').text()).toBe('validations.messages.min') - }) - - it('flushes no error message when memo is valid', async () => { - await wrapper.find('#input-group-3').find('textarea').setValue('Long enough') - await flushPromises() - expect(wrapper.find('span.errors').exists()).toBeFalsy() - }) - }) - - describe('cancel button', () => { - it('has a cancel button', () => { - expect(wrapper.find('button[type="reset"]').exists()).toBeTruthy() - }) - - it('has the text "form.cancel"', () => { - expect(wrapper.find('button[type="reset"]').text()).toBe('form.reset') - }) - - it('clears all fields on click', async () => { - await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv') - await wrapper.find('#input-group-2').find('input').setValue('87.23') - await wrapper.find('#input-group-3').find('textarea').setValue('Long enough') - await flushPromises() - expect(wrapper.vm.form.email).toBe('someone@watches.tv') - expect(wrapper.vm.form.amount).toBe('87.23') - expect(wrapper.vm.form.memo).toBe('Long enough') - await wrapper.find('button[type="reset"]').trigger('click') - await flushPromises() - expect(wrapper.vm.form.email).toBe('') - expect(wrapper.vm.form.amount).toBe('') - expect(wrapper.vm.form.memo).toBe('') - }) - }) - - describe('submit', () => { - beforeEach(async () => { - await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv') - await wrapper.find('#input-group-2').find('input').setValue('87.23') - await wrapper.find('#input-group-3').find('textarea').setValue('Long enough') - await wrapper.find('form').trigger('submit') - await flushPromises() - }) - - it('emits set-transaction', async () => { - expect(wrapper.emitted('set-transaction')).toBeTruthy() - expect(wrapper.emitted('set-transaction')).toEqual([ - [ - { - email: 'someone@watches.tv', - amount: 87.23, - memo: 'Long enough', - }, - ], - ]) - }) + it('has no input field of id input-group-1', () => { + expect(wrapper.find('#input-group-1').isVisible()).toBeFalsy() }) }) }) diff --git a/frontend/src/components/GddSend/TransactionForm.vue b/frontend/src/components/GddSend/TransactionForm.vue index 850bb3b07..991e165ce 100644 --- a/frontend/src/components/GddSend/TransactionForm.vue +++ b/frontend/src/components/GddSend/TransactionForm.vue @@ -2,26 +2,39 @@ - - + + + + {{ $t('send_gdd') }} + + + + + {{ $t('send_per_link') }} + + + +
+

{{ $t('gdd_per_link.header') }}

+
+ {{ $t('gdd_per_link.sentence_1') }} +
+
- +
-
{{ $t('form.no_gdd_available') }} @@ -125,7 +137,7 @@ - {{ $t('form.send_now') }} + {{ selected === sendTypes.send ? $t('form.send_now') : $t('form.generate_now') }} @@ -138,16 +150,13 @@ diff --git a/frontend/src/components/GddSend/TransactionResultLink.vue b/frontend/src/components/GddSend/TransactionResultLink.vue new file mode 100644 index 000000000..66b5529d6 --- /dev/null +++ b/frontend/src/components/GddSend/TransactionResultLink.vue @@ -0,0 +1,33 @@ + + diff --git a/frontend/src/components/GddSend/TransactionResult.vue b/frontend/src/components/GddSend/TransactionResultSendError.vue similarity index 70% rename from frontend/src/components/GddSend/TransactionResult.vue rename to frontend/src/components/GddSend/TransactionResultSendError.vue index 64a6749bd..ba6b4a238 100644 --- a/frontend/src/components/GddSend/TransactionResult.vue +++ b/frontend/src/components/GddSend/TransactionResultSendError.vue @@ -1,6 +1,6 @@ diff --git a/frontend/src/components/GddTransactionList.spec.js b/frontend/src/components/GddTransactionList.spec.js index 320fd094f..ab8f3658c 100644 --- a/frontend/src/components/GddTransactionList.spec.js +++ b/frontend/src/components/GddTransactionList.spec.js @@ -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', ) }) diff --git a/frontend/src/graphql/mutations.js b/frontend/src/graphql/mutations.js index c8f5455d7..d4bf8c1da 100644 --- a/frontend/src/graphql/mutations.js +++ b/frontend/src/graphql/mutations.js @@ -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 + } + } +` diff --git a/frontend/src/graphql/queries.js b/frontend/src/graphql/queries.js index 94a5e59f3..4a7051bdf 100644 --- a/frontend/src/graphql/queries.js +++ b/frontend/src/graphql/queries.js @@ -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 + } + } + } +` diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index fb3b82c89..94b1e8eb0 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -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 {count} Transaktionen ansehen" }, + "transaction-link": { + "button": "einlösen", + "send_you": "sendet dir" + }, "transactions": "Transaktionen", "whitepaper": "Whitepaper" } diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index aec28a51d..685ca499f 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -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 {count} transactions." }, + "transaction-link": { + "button": "redeem", + "send_you": "wants to send you" + }, "transactions": "Transactions", "whitepaper": "Whitepaper" } diff --git a/frontend/src/mixins/toaster.js b/frontend/src/mixins/toaster.js index 4464a2cc9..68fd78ff9 100644 --- a/frontend/src/mixins/toaster.js +++ b/frontend/src/mixins/toaster.js @@ -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, diff --git a/frontend/src/pages/Send.spec.js b/frontend/src/pages/Send.spec.js index c724d965f..cc18bed59 100644 --- a/frontend/src/pages/Send.spec.js +++ b/frontend/src/pages/Send.spec.js @@ -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!' }) + }) + }) + }) }) }) diff --git a/frontend/src/pages/Send.vue b/frontend/src/pages/Send.vue index 072342d7c..89eb1bbe2 100644 --- a/frontend/src/pages/Send.vue +++ b/frontend/src/pages/Send.vue @@ -2,27 +2,45 @@
- diff --git a/frontend/src/routes/router.test.js b/frontend/src/routes/router.test.js index 665848136..4c454a9b4 100644 --- a/frontend/src/routes/router.test.js +++ b/frontend/src/routes/router.test.js @@ -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', () => { diff --git a/frontend/src/routes/routes.js b/frontend/src/routes/routes.js index 3fc8dc766..ea7539190 100755 --- a/frontend/src/routes/routes.js +++ b/frontend/src/routes/routes.js @@ -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 }, ]