-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
972 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { BankAccountNumberRequest, BankAccountResponse, BankIbanRequest } from '@src/view_models/BankAccount'; | ||
import axios from 'axios'; | ||
|
||
export interface BankValidationResource { | ||
validateIban: ( data: BankIbanRequest ) => Promise<BankAccountResponse>; | ||
validateBankNumber: ( data: BankAccountNumberRequest ) => Promise<BankAccountResponse>; | ||
} | ||
|
||
export interface APIResponse { | ||
status: string; | ||
iban: string; | ||
bic: string; | ||
account: string; | ||
bankCode: string; | ||
bankName?: string; | ||
} | ||
|
||
export class ApiBankValidationResource implements BankValidationResource { | ||
validateIbanEndpoint: string; | ||
validateBankNumberEndpoint: string; | ||
|
||
constructor( validateIbanEndpoint: string, validateBankNumberEndpoint: string ) { | ||
this.validateIbanEndpoint = validateIbanEndpoint; | ||
this.validateBankNumberEndpoint = validateBankNumberEndpoint; | ||
} | ||
|
||
async validateIban( data: BankIbanRequest ): Promise<BankAccountResponse> { | ||
return this.validateBankData( this.validateIbanEndpoint, data ).then( ( response: APIResponse ) => { | ||
return { | ||
accountNumber: response.account, | ||
bankCode: response.bankCode, | ||
bankName: response.bankName, | ||
iban: response.iban, | ||
bic: response.bic, | ||
}; | ||
} ); | ||
} | ||
|
||
async validateBankNumber( data: BankAccountNumberRequest ): Promise<BankAccountResponse> { | ||
return this.validateBankData( this.validateBankNumberEndpoint, data ).then( ( response: APIResponse ) => { | ||
return { | ||
accountNumber: response.account, | ||
bankCode: response.bankCode, | ||
bankName: response.bankName, | ||
iban: response.iban, | ||
bic: response.bic, | ||
}; | ||
} ); | ||
} | ||
|
||
async validateBankData( endpoint: string, data: BankAccountNumberRequest | BankIbanRequest ): Promise<APIResponse> { | ||
let validationResult = await axios( endpoint, { | ||
method: 'get', | ||
headers: { 'Content-Type': 'multipart/form-data' }, | ||
params: data, | ||
} ); | ||
|
||
if ( validationResult.data.status === 'ERR' ) { | ||
return Promise.reject( 'ERR' ); | ||
} | ||
|
||
return Promise.resolve( validationResult.data ); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
<template> | ||
<div class="payment-bank-data-section"> | ||
<ScrollTarget target-id="payment-form-iban-scroll-target"/> | ||
<DirectDebitField | ||
v-model:account-number="accountNumber" | ||
v-model:bank-code="bankCode" | ||
:bank-name="bankName" | ||
:bic="bic" | ||
:account-number-type="accountNumberType" | ||
:show-account-number-error="showError" | ||
:show-bank-code-error="showError" | ||
:bank-validation-resource="bankValidationResource" | ||
@field-changed="validateFields" | ||
/> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import DirectDebitField from '@src/components/shared/form_fields/DirectDebitField.vue'; | ||
import { computed, inject, ref } from 'vue'; | ||
import ScrollTarget from '@src/components/shared/ScrollTarget.vue'; | ||
import { useStore } from 'vuex'; | ||
import { BankValidationResource } from '@src/api/BankValidationResource'; | ||
import { action } from '@src/store/util'; | ||
import { looksLikeBankAccountNumber, looksLikeIban } from '@src/util/bank_account_number_helpers'; | ||
import { AccountNumberType, BankAccountResponse } from '@src/view_models/BankAccount'; | ||
import { Validity } from '@src/view_models/Validity'; | ||
const store = useStore(); | ||
const bankValidationResource = inject<BankValidationResource>( 'bankValidationResource' ); | ||
const accountNumber = ref<string>( store.getters[ 'bankdata/accountNumber' ] ); | ||
const bankCode = ref<string>( store.getters[ 'bankdata/bankCode' ] ); | ||
const bankName = computed<string>( () => store.getters[ 'bankdata/bankName' ] ); | ||
const bic = computed<string>( () => store.getters[ 'bankdata/bic' ] ); | ||
const showError = computed<boolean>( () => store.getters[ 'bankdata/bankDataIsInvalid' ] ); | ||
const accountNumberType = computed<AccountNumberType>( () => { | ||
if ( looksLikeIban( accountNumber.value ) ) { | ||
return AccountNumberType.IBAN; | ||
} | ||
if ( looksLikeBankAccountNumber( accountNumber.value ) ) { | ||
return AccountNumberType.Account; | ||
} | ||
return AccountNumberType.None; | ||
} ); | ||
const validateFields = async ( fieldName: 'account-number' | 'bank-code' ): Promise<void> => { | ||
if ( fieldName === 'account-number' ) { | ||
await store.dispatch( action( 'bankdata', 'setAccountNumber' ), accountNumber.value ); | ||
} | ||
if ( fieldName === 'bank-code' ) { | ||
await store.dispatch( action( 'bankdata', 'setBankCode' ), bankCode.value ); | ||
} | ||
if ( accountNumberType.value !== AccountNumberType.Account || fieldName === 'bank-code' ) { | ||
await store.dispatch( action( 'bankdata', 'markEmptyFieldsAsInvalid' ) ); | ||
} | ||
if ( !store.getters[ 'bankdata/bankDataIsValid' ] ) { | ||
return Promise.resolve(); | ||
} | ||
let response: BankAccountResponse; | ||
await store.dispatch( action( 'bankdata', 'setValidating' ), true ); | ||
try { | ||
if ( accountNumberType.value === AccountNumberType.IBAN ) { | ||
response = await bankValidationResource.validateIban( { | ||
iban: accountNumber.value.toUpperCase(), | ||
} ); | ||
await store.dispatch( action( 'bankdata', 'setAccountNumber' ), response.iban ); | ||
await store.dispatch( action( 'bankdata', 'setBankCode' ), '' ); | ||
} else { | ||
response = await bankValidationResource.validateBankNumber( { | ||
accountNumber: accountNumber.value, | ||
bankCode: bankCode.value, | ||
} ); | ||
await store.dispatch( action( 'bankdata', 'setAccountNumber' ), response.accountNumber ); | ||
await store.dispatch( action( 'bankdata', 'setBankCode' ), response.bankCode ); | ||
} | ||
await store.dispatch( action( 'bankdata', 'setBankDataValidity' ), Validity.VALID ); | ||
await store.dispatch( action( 'bankdata', 'setBankName' ), response.bankName ); | ||
await store.dispatch( action( 'bankdata', 'setIban' ), response.iban ); | ||
await store.dispatch( action( 'bankdata', 'setBic' ), response.bic ); | ||
} catch ( e ) { | ||
await store.dispatch( action( 'bankdata', 'setBankDataValidity' ), Validity.INVALID ); | ||
await store.dispatch( action( 'bankdata', 'setBankName' ), '' ); | ||
await store.dispatch( action( 'bankdata', 'setIban' ), '' ); | ||
await store.dispatch( action( 'bankdata', 'setBic' ), '' ); | ||
} | ||
await store.dispatch( action( 'bankdata', 'setValidating' ), false ); | ||
}; | ||
store.watch( ( state, getters ) => getters[ 'bankdata/accountNumber' ], ( newAccountNumber: string ) => { | ||
accountNumber.value = newAccountNumber; | ||
} ); | ||
store.watch( ( state, getters ) => getters[ 'bankdata/bankCode' ], ( newBankCode: string ) => { | ||
bankCode.value = newBankCode; | ||
} ); | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
<template> | ||
<div class="form-field-direct-debit"> | ||
<TextValueField | ||
:value="formattedBankNumber" | ||
:cursor-position="bankNumberCursorPosition" | ||
input-id="account-number" | ||
name="account-number" | ||
:label="$t( accountNumberLabel )" | ||
:placeholder="$t( 'donation_form_payment_bankdata_account_iban_placeholder' )" | ||
:error-message="$t( 'donation_form_payment_bankdata_error' )" | ||
:show-error="showAccountNumberError" | ||
help-text="Please enter your IBAN or your German bank account number" | ||
@field-changed="$emit( 'field-changed', 'account-number' )" | ||
@input="onAccountNumberInput" | ||
> | ||
<template #message> | ||
<span v-if="bankName !== '' && accountNumberType === AccountNumberType.IBAN" class="iban-bank-name">{{ bankName }} ({{ bic }})</span> | ||
</template> | ||
</TextValueField> | ||
|
||
<TextField | ||
v-if="accountNumberType === AccountNumberType.Account" | ||
v-model="bankCode" | ||
input-id="bank-code" | ||
name="bank-code" | ||
:label="$t( 'donation_form_payment_bankdata_bank_legacy_label' )" | ||
:placeholder="$t( 'donation_form_payment_bankdata_bank_legacy_placeholder' )" | ||
error-message="" | ||
:show-error="showBankCodeError" | ||
help-text="It looks like you entered a bank account number, if so please enter your bank code" | ||
@field-changed="$emit( 'field-changed', 'bank-code' )" | ||
@update:model-value="onBankCodeUpdate" | ||
> | ||
<template #message> | ||
<span v-if="bankName !== ''" class="bank-name">{{ bankName }}</span> | ||
</template> | ||
</TextField> | ||
</div> | ||
</template> | ||
|
||
<script setup lang="ts"> | ||
import TextField from '@src/components/shared/form_fields/TextField.vue'; | ||
import { computed, nextTick, ref, watch } from 'vue'; | ||
import { BankValidationResource } from '@src/api/BankValidationResource'; | ||
import TextValueField from '@src/components/shared/form_fields/TextValueField.vue'; | ||
import { useFieldModel } from '@src/components/shared/form_fields/useFieldModel'; | ||
import { AccountNumberType } from '@src/view_models/BankAccount'; | ||
interface Props { | ||
accountNumber: string; | ||
bankCode: string; | ||
bankName: string; | ||
bic: string; | ||
accountNumberType: AccountNumberType | ||
showAccountNumberError: boolean; | ||
showBankCodeError: boolean; | ||
bankValidationResource: BankValidationResource; | ||
} | ||
const getDisplayValue = ( newValue: string ) => { | ||
return newValue.replace( /(.{4})/g, '$& ' ).trim(); | ||
}; | ||
const props = defineProps<Props>(); | ||
const emit = defineEmits( [ 'field-changed', 'update:accountNumber', 'update:bankCode' ] ); | ||
const accountNumber = useFieldModel<string>( () => props.accountNumber, props.accountNumber ); | ||
const bankCode = useFieldModel<string>( () => props.bankCode, props.bankCode ); | ||
const formattedBankNumber = ref<string>( getDisplayValue( accountNumber.value ) ); | ||
const bankNumberCursorPosition = ref<number>( 0 ); | ||
const accountNumberLabel = computed<string>( () => { | ||
if ( props.accountNumberType === AccountNumberType.IBAN ) { | ||
return 'donation_form_payment_bankdata_account_iban_label'; | ||
} else if ( props.accountNumberType === AccountNumberType.Account ) { | ||
return 'donation_form_payment_bankdata_account_legacy_label'; | ||
} | ||
return 'donation_form_payment_bankdata_account_default_label'; | ||
} ); | ||
/** | ||
* 1. Get the text before the cursor | ||
* 2. Clear the spaces | ||
* 3. Add new spaces in the correct positions, but don't trim | ||
* 4. Return the length | ||
* | ||
* @param newValue | ||
* @param cursorPosition | ||
*/ | ||
const getNewCursorPosition = ( newValue: string, cursorPosition: number ): number => { | ||
return newValue | ||
.slice( 0, cursorPosition ) | ||
.replace( /\s/g, '' ) | ||
.replace( /(.{4})/g, '$& ' ) | ||
.length; | ||
}; | ||
const onAccountNumberInput = async ( newValue: string, cursorPosition: number ): Promise<void> => { | ||
accountNumber.value = newValue.replace( /\s/g, '' ); | ||
await nextTick(); | ||
formattedBankNumber.value = getDisplayValue( accountNumber.value ); | ||
await nextTick(); | ||
// When we replace the field value the browser jumps the cursor to the end | ||
// so we reset it after changing the value. This allows the donor to edit | ||
// their bank number if they spot a mistake. | ||
bankNumberCursorPosition.value = getNewCursorPosition( newValue, cursorPosition ); | ||
emit( 'update:accountNumber', accountNumber.value ); | ||
}; | ||
const onBankCodeUpdate = ( newBankCode: string ): void => { | ||
emit( 'update:bankCode', newBankCode ); | ||
}; | ||
watch( accountNumber, ( newAccountNumber: string ) => { | ||
formattedBankNumber.value = getDisplayValue( newAccountNumber ); | ||
} ); | ||
</script> | ||
|
||
<style lang="scss"> | ||
@use '@src/scss/settings/forms'; | ||
@use 'sass:map'; | ||
.form-field-direct-debit { | ||
max-width: map.get( forms.$input, 'max-width' ); | ||
} | ||
</style> |
Oops, something went wrong.