Merge pull request #1988 from gradido/merge-frontend-redeem-contribution-link

frontend redeem contribution link
This commit is contained in:
Alexander Friedland 2022-06-17 15:07:29 +02:00 committed by GitHub
commit 64605afc16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 255 additions and 64 deletions

View File

@ -181,6 +181,7 @@ export default {
.then((result) => { .then((result) => {
this.link = result.data.createContributionLink.link this.link = result.data.createContributionLink.link
this.toastSuccess(this.link) this.toastSuccess(this.link)
this.onReset()
}) })
.catch((error) => { .catch((error) => {
this.toastError(error.message) this.toastError(error.message)

View File

@ -276,8 +276,8 @@ export class TransactionLinkResolver {
logger.info('creation from contribution link commited successfuly.') logger.info('creation from contribution link commited successfuly.')
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
logger.error(`Creation from contribution link was not successful: ${e}`) logger.error(`Creation from contribution link was not successful: ${e}`)
throw new Error(`Creation from contribution link was not successful. ${e}`) throw new Error(`Creation from contribution link was not successful. ${e}`)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }

View File

@ -0,0 +1,133 @@
import { mount } from '@vue/test-utils'
import LanguageSwitch from './LanguageSwitch2'
const localVue = global.localVue
const updateUserInfosMutationMock = jest.fn().mockResolvedValue({
data: {
updateUserInfos: {
validValues: 1,
},
},
})
describe('LanguageSwitch', () => {
let wrapper
const state = {
email: 'he@ho.he',
language: null,
}
const mocks = {
$store: {
state,
commit: jest.fn(),
},
$i18n: {
locale: 'en',
},
$t: jest.fn((t) => t),
$apollo: {
mutate: updateUserInfosMutationMock,
},
}
const Wrapper = () => {
return mount(LanguageSwitch, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div.language-switch').exists()).toBe(true)
})
describe('with locales en and de', () => {
describe('empty store', () => {
describe('navigator language is "en-US"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows English as default navigator langauge', async () => {
languageGetter.mockReturnValue('en-US')
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
})
})
describe('navigator language is "de-DE"', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows Deutsch as language ', async () => {
languageGetter.mockReturnValue('de-DE')
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
})
})
describe('navigator language is "es-ES" (not supported)', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows English as language ', async () => {
languageGetter.mockReturnValue('es-ES')
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
})
})
describe('no navigator langauge', () => {
const languageGetter = jest.spyOn(navigator, 'language', 'get')
it('shows English as language ', async () => {
languageGetter.mockReturnValue(null)
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
})
})
})
describe('language "de" in store', () => {
it('shows Deutsch as language', async () => {
wrapper.vm.$store.state.language = 'de'
wrapper.vm.setCurrentLanguage()
await wrapper.vm.$nextTick()
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
})
})
describe('language menu', () => {
it('has English and German as languages to choose', () => {
expect(wrapper.findAll('span.locales')).toHaveLength(2)
})
it('has English as first language to choose', () => {
expect(wrapper.findAll('span.locales').at(0).text()).toBe('English')
})
it('has German as second language to choose', () => {
expect(wrapper.findAll('span.locales').at(1).text()).toBe('Deutsch')
})
})
})
describe('calls the API', () => {
it("with locale 'de'", () => {
wrapper.findAll('span.locales').at(1).trigger('click')
expect(updateUserInfosMutationMock).toBeCalledWith(
expect.objectContaining({
variables: {
locale: 'de',
},
}),
)
})
// it("with locale 'en'", () => {
// wrapper.findAll('span.locales').at(0).trigger('click')
// expect(updateUserInfosMutationMock).toBeCalledWith(
// expect.objectContaining({
// variables: {
// locale: 'en',
// },
// }),
// )
// })
})
})
})

View File

@ -7,7 +7,7 @@
class="pointer pr-3" class="pointer pr-3"
:class="$store.state.language === lang.code ? 'c-blau' : 'c-grey'" :class="$store.state.language === lang.code ? 'c-blau' : 'c-grey'"
> >
{{ lang.name }} <span class="locales">{{ lang.name }}</span>
<span class="ml-3">{{ locales.length - 1 > index ? $t('math.pipe') : '' }}</span> <span class="ml-3">{{ locales.length - 1 > index ? $t('math.pipe') : '' }}</span>
</span> </span>
</div> </div>

View File

@ -1,8 +1,12 @@
<template> <template>
<div class="redeem-information"> <div class="redeem-information">
<b-jumbotron bg-variant="muted" text-variant="dark" border-variant="info"> <b-jumbotron bg-variant="muted" text-variant="dark" border-variant="info">
<h1> <h1 v-if="isContributionLink">
{{ firstName }} {{ CONFIG.COMMUNITY_NAME }}
{{ $t('contribution-link.thanksYouWith') }} {{ amount | GDD }}
</h1>
<h1 v-else>
{{ user.firstName }}
{{ $t('transaction-link.send_you') }} {{ amount | GDD }} {{ $t('transaction-link.send_you') }} {{ amount | GDD }}
</h1> </h1>
<b>{{ memo }}</b> <b>{{ memo }}</b>
@ -10,12 +14,20 @@
</div> </div>
</template> </template>
<script> <script>
import CONFIG from '@/config'
export default { export default {
name: 'RedeemInformation', name: 'RedeemInformation',
props: { props: {
firstName: { type: String, required: true }, user: { type: Object, required: false },
amount: { type: String, required: true }, amount: { type: String, required: true },
memo: { type: String, required: true, default: '' }, memo: { type: String, required: true, default: '' },
isContributionLink: { type: Boolean, default: false },
},
data() {
return {
CONFIG,
}
}, },
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="redeem-logged-out"> <div class="redeem-logged-out">
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" /> <redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
<b-jumbotron> <b-jumbotron>
<div class="mb-6"> <div class="mb-6">
@ -32,9 +32,8 @@ export default {
RedeemInformation, RedeemInformation,
}, },
props: { props: {
user: { type: Object, required: true }, linkData: { type: Object, required: true },
amount: { type: String, required: true }, isContributionLink: { type: Boolean, default: false },
memo: { type: String, required: true, default: '' },
}, },
computed: { computed: {
login() { login() {

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="redeem-self-creator"> <div class="redeem-self-creator">
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" /> <redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
<b-jumbotron> <b-jumbotron>
<div class="mb-3 text-center"> <div class="mb-3 text-center">
@ -23,9 +23,8 @@ export default {
RedeemInformation, RedeemInformation,
}, },
props: { props: {
user: { type: Object, required: true }, linkData: { type: Object, required: true },
amount: { type: String, required: true }, isContributionLink: { type: Boolean, default: false },
memo: { type: String, required: true, default: '' },
}, },
} }
</script> </script>

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="redeem-valid"> <div class="redeem-valid">
<redeem-information :firstName="user.firstName" :amount="amount" :memo="memo" /> <redeem-information v-bind="linkData" :isContributionLink="isContributionLink" />
<b-jumbotron> <b-jumbotron>
<div class="mb-3 text-center"> <div class="mb-3 text-center">
<b-button variant="primary" @click="$emit('redeem-link', amount)" size="lg"> <b-button variant="primary" @click="$emit('redeem-link', linkData.amount)" size="lg">
{{ $t('gdd_per_link.redeem') }} {{ $t('gdd_per_link.redeem') }}
</b-button> </b-button>
</div> </div>
@ -19,9 +19,8 @@ export default {
RedeemInformation, RedeemInformation,
}, },
props: { props: {
user: { type: Object, required: false }, linkData: { type: Object, required: true },
amount: { type: String, required: false }, isContributionLink: { type: Boolean, default: false },
memo: { type: String, required: false, default: '' },
}, },
} }
</script> </script>

View File

@ -114,17 +114,33 @@ export const queryOptIn = gql`
export const queryTransactionLink = gql` export const queryTransactionLink = gql`
query($code: String!) { query($code: String!) {
queryTransactionLink(code: $code) { queryTransactionLink(code: $code) {
id ... on TransactionLink {
amount id
memo amount
createdAt memo
validUntil createdAt
redeemedAt validUntil
deletedAt redeemedAt
user { deletedAt
firstName user {
publisherId firstName
email publisherId
email
}
}
... on ContributionLink {
id
validTo
validFrom
amount
name
memo
cycle
createdAt
code
link
deletedAt
maxAmountPerMonth
} }
} }
} }

View File

@ -26,6 +26,9 @@
"other-communities": "Weitere Gemeinschaften", "other-communities": "Weitere Gemeinschaften",
"switch-to-this-community": "zu dieser Gemeinschaft wechseln" "switch-to-this-community": "zu dieser Gemeinschaft wechseln"
}, },
"contribution-link": {
"thanksYouWith": "dankt dir mit"
},
"decay": { "decay": {
"before_startblock_transaction": "Diese Transaktion beinhaltet keine Vergänglichkeit.", "before_startblock_transaction": "Diese Transaktion beinhaltet keine Vergänglichkeit.",
"calculation_decay": "Berechnung der Vergänglichkeit", "calculation_decay": "Berechnung der Vergänglichkeit",

View File

@ -26,6 +26,9 @@
"other-communities": "Other communities", "other-communities": "Other communities",
"switch-to-this-community": "Switch to this community" "switch-to-this-community": "Switch to this community"
}, },
"contribution-link": {
"thanksYouWith": "thanks you with"
},
"decay": { "decay": {
"before_startblock_transaction": "This transaction does not include decay.", "before_startblock_transaction": "This transaction does not include decay.",
"calculation_decay": "Calculation of Decay", "calculation_decay": "Calculation of Decay",

View File

@ -88,7 +88,11 @@ export default {
? this.$t('message.checkEmail') ? this.$t('message.checkEmail')
: this.$t('message.reset') : this.$t('message.reset')
this.messageButtonText = this.$t('login') this.messageButtonText = this.$t('login')
this.messageButtonLinktTo = '/login' if (this.$route.params.code) {
this.messageButtonLinktTo = `/login/${this.$route.params.code}`
} else {
this.messageButtonLinktTo = '/login'
}
}) })
.catch((error) => { .catch((error) => {
let errorMessage let errorMessage

View File

@ -24,6 +24,7 @@ const transactionLinkValidExpireDate = () => {
apolloQueryMock.mockResolvedValue({ apolloQueryMock.mockResolvedValue({
data: { data: {
queryTransactionLink: { queryTransactionLink: {
__typename: 'TransactionLink',
id: 92, id: 92,
amount: '22', amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
@ -82,6 +83,7 @@ describe('TransactionLink', () => {
variables: { variables: {
code: 'some-code', code: 'some-code',
}, },
fetchPolicy: 'no-cache',
}) })
}) })
@ -90,6 +92,7 @@ describe('TransactionLink', () => {
apolloQueryMock.mockResolvedValue({ apolloQueryMock.mockResolvedValue({
data: { data: {
queryTransactionLink: { queryTransactionLink: {
__typename: 'TransactionLink',
id: 92, id: 92,
amount: '22', amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
@ -120,6 +123,7 @@ describe('TransactionLink', () => {
apolloQueryMock.mockResolvedValue({ apolloQueryMock.mockResolvedValue({
data: { data: {
queryTransactionLink: { queryTransactionLink: {
__typename: 'TransactionLink',
id: 92, id: 92,
amount: '22', amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
@ -150,6 +154,7 @@ describe('TransactionLink', () => {
apolloQueryMock.mockResolvedValue({ apolloQueryMock.mockResolvedValue({
data: { data: {
queryTransactionLink: { queryTransactionLink: {
__typename: 'TransactionLink',
id: 92, id: 92,
amount: '22', amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
@ -177,9 +182,11 @@ describe('TransactionLink', () => {
describe('no token in store', () => { describe('no token in store', () => {
beforeEach(() => { beforeEach(() => {
mocks.$store.state.token = null
apolloQueryMock.mockResolvedValue({ apolloQueryMock.mockResolvedValue({
data: { data: {
queryTransactionLink: { queryTransactionLink: {
__typename: 'TransactionLink',
id: 92, id: 92,
amount: '22', amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
@ -213,6 +220,7 @@ describe('TransactionLink', () => {
apolloQueryMock.mockResolvedValue({ apolloQueryMock.mockResolvedValue({
data: { data: {
queryTransactionLink: { queryTransactionLink: {
__typename: 'TransactionLink',
id: 92, id: 92,
amount: '22', amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
@ -248,6 +256,7 @@ describe('TransactionLink', () => {
apolloQueryMock.mockResolvedValue({ apolloQueryMock.mockResolvedValue({
data: { data: {
queryTransactionLink: { queryTransactionLink: {
__typename: 'TransactionLink',
id: 92, id: 92,
amount: '22', amount: '22',
memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ', memo: 'Abrakadabra drei, vier, fünf, sechs, hier steht jetzt ein Memotext! Hex hex ',
@ -298,7 +307,7 @@ describe('TransactionLink', () => {
}) })
it('toasts a success message', () => { it('toasts a success message', () => {
expect(mocks.$t).toBeCalledWith('gdd_per_link.redeemed', { n: '22' }) expect(mocks.$t).toBeCalledWith('gdd_per_link.redeem')
expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ') expect(toastSuccessSpy).toBeCalledWith('gdd_per_link.redeemed; ')
}) })

View File

@ -1,18 +1,21 @@
<template> <template>
<div class="show-transaction-link-informations"> <div class="show-transaction-link-informations">
<div class="text-center"><b-img :src="img" fluid alt="logo"></b-img></div>
<b-container class="mt-4"> <b-container class="mt-4">
<transaction-link-item :type="itemType"> <transaction-link-item :type="itemType">
<template #LOGGED_OUT> <template #LOGGED_OUT>
<redeem-logged-out v-bind="linkData" /> <redeem-logged-out :linkData="linkData" :isContributionLink="isContributionLink" />
</template> </template>
<template #SELF_CREATOR> <template #SELF_CREATOR>
<redeem-self-creator v-bind="linkData" /> <redeem-self-creator :linkData="linkData" />
</template> </template>
<template #VALID> <template #VALID>
<redeem-valid v-bind="linkData" @redeem-link="redeemLink" /> <redeem-valid
:linkData="linkData"
:isContributionLink="isContributionLink"
@redeem-link="redeemLink"
/>
</template> </template>
<template #TEXT> <template #TEXT>
@ -44,6 +47,7 @@ export default {
return { return {
img: '/img/brand/green.png', img: '/img/brand/green.png',
linkData: { linkData: {
__typename: 'TransactionLink',
amount: '123.45', amount: '123.45',
memo: 'memo', memo: 'memo',
user: { user: {
@ -57,6 +61,7 @@ export default {
setTransactionLinkInformation() { setTransactionLinkInformation() {
this.$apollo this.$apollo
.query({ .query({
fetchPolicy: 'no-cache',
query: queryTransactionLink, query: queryTransactionLink,
variables: { variables: {
code: this.$route.params.code, code: this.$route.params.code,
@ -64,37 +69,45 @@ export default {
}) })
.then((result) => { .then((result) => {
this.linkData = result.data.queryTransactionLink this.linkData = result.data.queryTransactionLink
if (this.linkData.__typename === 'ContributionLink' && this.$store.state.token) {
this.mutationLink(this.linkData.amount)
}
}) })
.catch((err) => { .catch((err) => {
this.toastError(err.message) this.toastError(err.message)
}) })
}, },
mutationLink(amount) {
this.$apollo
.mutate({
mutation: redeemTransactionLink,
variables: {
code: this.$route.params.code,
},
})
.then(() => {
this.toastSuccess(
this.$t('gdd_per_link.redeemed', {
n: amount,
}),
)
this.$router.push('/overview')
})
.catch((err) => {
this.toastError(err.message)
this.$router.push('/overview')
})
},
redeemLink(amount) { redeemLink(amount) {
this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.redeem-text')).then(async (value) => { this.$bvModal.msgBoxConfirm(this.$t('gdd_per_link.redeem-text')).then((value) => {
if (value) if (value) this.mutationLink(amount)
await this.$apollo
.mutate({
mutation: redeemTransactionLink,
variables: {
code: this.$route.params.code,
},
})
.then(() => {
this.toastSuccess(
this.$t('gdd_per_link.redeemed', {
n: amount,
}),
)
this.$router.push('/overview')
})
.catch((err) => {
this.toastError(err.message)
this.$router.push('/overview')
})
}) })
}, },
}, },
computed: { computed: {
isContributionLink() {
return this.$route.params.code.search(/^CL-/) === 0
},
itemType() { itemType() {
// link wurde gelöscht: am, von // link wurde gelöscht: am, von
if (this.linkData.deletedAt) { if (this.linkData.deletedAt) {
@ -124,16 +137,12 @@ export default {
if (this.$store.state.token) { if (this.$store.state.token) {
// logged in, nicht berechtigt einzulösen, eigener link // logged in, nicht berechtigt einzulösen, eigener link
if (this.$store.state.email === this.linkData.user.email) { if (this.linkData.user && this.$store.state.email === this.linkData.user.email) {
return `SELF_CREATOR` return `SELF_CREATOR`
} }
// logged in und berechtigt einzulösen // logged in und berechtigt einzulösen
if ( if (!this.linkData.redeemedAt && !this.linkData.deletedAt) {
this.$store.state.email !== this.linkData.user.email &&
!this.linkData.redeemedAt &&
!this.linkData.deletedAt
) {
return `VALID` return `VALID`
} }
} }

View File

@ -29,7 +29,11 @@ const authLink = new ApolloLink((operation, forward) => {
const apolloClient = new ApolloClient({ const apolloClient = new ApolloClient({
link: authLink.concat(httpLink), link: authLink.concat(httpLink),
cache: new InMemoryCache(), cache: new InMemoryCache({
possibleTypes: {
QueryLinkResult: ['TransactionLink', 'ContributionLink'],
},
}),
}) })
export const apolloProvider = new VueApollo({ export const apolloProvider = new VueApollo({