Merge pull request #251 from woocommerce/PCP-244-saved-credit-card-does-not-auto-

Improve UX for stored credit card payments
This commit is contained in:
Emili Castells 2021-09-16 15:46:18 +02:00 committed by GitHub
commit caa33ab51e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 167 additions and 63 deletions

View file

@ -139,24 +139,7 @@ class PaymentTokenEndpoint {
foreach ( $json->payment_tokens as $token_value ) {
$tokens[] = $this->factory->from_paypal_response( $token_value );
}
if ( empty( $tokens ) ) {
$error = new RuntimeException(
sprintf(
// translators: %d is the customer id.
__( 'No token stored for customer %d.', 'woocommerce-paypal-payments' ),
$id
)
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
return $tokens;
}

View file

@ -7,3 +7,7 @@
.payments-sdk-contingency-handler {
z-index: 1000 !important;
}
.ppcp-credit-card-gateway-form-field-disabled {
opacity: .5 !important;
}

View file

@ -24,9 +24,11 @@ class CheckoutBootstap {
})
jQuery('#saved-credit-card').on('change', () => {
this.displayPlaceOrderButtonForSavedCreditCards()
})
jQuery(document).on('hosted_fields_loaded', () => {
jQuery('#saved-credit-card').on('change', () => {
this.displayPlaceOrderButtonForSavedCreditCards()
})
});
this.switchBetweenPayPalandOrderButton()
this.displayPlaceOrderButtonForSavedCreditCards()
@ -100,13 +102,41 @@ class CheckoutBootstap {
this.renderer.hideButtons(this.gateway.messages.wrapper)
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper)
jQuery('#place_order').show()
this.disableCreditCardFields()
} else {
jQuery('#place_order').hide()
this.renderer.hideButtons(this.gateway.button.wrapper)
this.renderer.hideButtons(this.gateway.messages.wrapper)
this.renderer.showButtons(this.gateway.hosted_fields.wrapper)
this.enableCreditCardFields()
}
}
disableCreditCardFields() {
jQuery('label[for="ppcp-credit-card-gateway-card-number"]').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-number').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-expiry').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-cvc').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="vault"]').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-vault').addClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-vault').attr("disabled", true)
this.renderer.disableCreditCardFields()
}
enableCreditCardFields() {
jQuery('label[for="ppcp-credit-card-gateway-card-number"]').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-number').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="ppcp-credit-card-gateway-card-expiry"]').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-expiry').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="ppcp-credit-card-gateway-card-cvc"]').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-gateway-card-cvc').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('label[for="vault"]').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-vault').removeClass('ppcp-credit-card-gateway-form-field-disabled')
jQuery('#ppcp-credit-card-vault').attr("disabled", false)
this.renderer.enableCreditCardFields()
}
}
export default CheckoutBootstap

View file

@ -100,6 +100,7 @@ class CreditCardRenderer {
}
}
}).then(hostedFields => {
document.dispatchEvent(new CustomEvent("hosted_fields_loaded"));
this.currentHostedFieldsInstance = hostedFields;
hostedFields.on('inputSubmitRequest', () => {
@ -141,6 +142,36 @@ class CreditCardRenderer {
)
}
disableFields() {
this.currentHostedFieldsInstance.setAttribute({
field: 'number',
attribute: 'disabled'
})
this.currentHostedFieldsInstance.setAttribute({
field: 'cvv',
attribute: 'disabled'
})
this.currentHostedFieldsInstance.setAttribute({
field: 'expirationDate',
attribute: 'disabled'
})
}
enableFields() {
this.currentHostedFieldsInstance.removeAttribute({
field: 'number',
attribute: 'disabled'
})
this.currentHostedFieldsInstance.removeAttribute({
field: 'cvv',
attribute: 'disabled'
})
this.currentHostedFieldsInstance.removeAttribute({
field: 'expirationDate',
attribute: 'disabled'
})
}
_submit(contextConfig) {
this.spinner.block();
this.errorHandler.clear();

View file

@ -43,6 +43,14 @@ class Renderer {
domElement.style.display = 'block';
return true;
}
disableCreditCardFields() {
this.creditCardRenderer.disableFields();
}
enableCreditCardFields() {
this.creditCardRenderer.enableFields();
}
}
export default Renderer;
export default Renderer;

View file

@ -107,13 +107,8 @@ class PaymentTokenRepository {
* @param PaymentToken[] $tokens The tokens.
* @return bool Whether tokens contains card or not.
*/
public function tokens_contains_card( $tokens ): bool {
foreach ( $tokens as $token ) {
if ( isset( $token->source()->card ) ) {
return true;
}
}
return false;
public function tokens_contains_card( array $tokens ): bool {
return $this->token_contains_source( $tokens, 'card' );
}
/**
@ -122,13 +117,8 @@ class PaymentTokenRepository {
* @param PaymentToken[] $tokens The tokens.
* @return bool Whether tokens contains card or not.
*/
public function tokens_contains_paypal( $tokens ): bool {
foreach ( $tokens as $token ) {
if ( isset( $token->source()->paypal ) ) {
return true;
}
}
return false;
public function tokens_contains_paypal( array $tokens ): bool {
return $this->token_contains_source( $tokens, 'paypal' );
}
/**
@ -145,4 +135,21 @@ class PaymentTokenRepository {
update_user_meta( $id, self::USER_META, $token_array );
return $token;
}
/**
* Checks if tokens has the given source.
*
* @param array $tokens Payment tokens.
* @param string $source_type Payment token source type.
* @return bool Whether tokens contains source or not.
*/
private function token_contains_source( array $tokens, string $source_type ): bool {
foreach ( $tokens as $token ) {
if ( isset( $token->source()->card ) && 'card' === $source_type || isset( $token->source()->paypal ) && 'paypal' === $source_type ) {
return true;
}
}
return false;
}
}

View file

@ -127,33 +127,6 @@ class PaymentTokenEndpointTest extends TestCase
$this->sut->for_user($id);
}
public function testForUserFailBecauseEmptyTokens()
{
$id = 1;
$token = Mockery::mock(Token::class);
$headers = Mockery::mock(Requests_Utility_CaseInsensitiveDictionary::class);
$headers->shouldReceive('getAll');
$rawResponse = [
'body' => '{"payment_tokens":[]}',
'headers' => $headers,
];
$this->bearer->shouldReceive('bearer')
->andReturn($token);
$token->shouldReceive('token')
->andReturn('bearer');
$this->ensureRequestForUser($rawResponse, $id);
expect('wp_remote_get')->andReturn($rawResponse);
expect('is_wp_error')->with($rawResponse)->andReturn(false);
expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(200);
$this->logger->shouldReceive('log');
$this->logger->shouldReceive('debug');
$this->expectException(RuntimeException::class);
$this->sut->for_user($id);
}
public function testDeleteToken()
{
$paymentToken = Mockery::mock(PaymentToken::class);

View file

@ -90,4 +90,72 @@ class PaymentTokenRepositoryTest extends TestCase
$this->sut->delete_token($id, $paymentToken);
}
public function testAllForUserId()
{
$id = 1;
$tokens = [];
$this->endpoint->shouldReceive('for_user')
->with($id)
->andReturn($tokens);
expect('update_user_meta')->with($id, $this->sut::USER_META, $tokens);
$result = $this->sut->all_for_user_id($id);
$this->assertSame($tokens, $result);
}
public function test_AllForUserIdReturnsEmptyArrayIfGettingTokenFails()
{
$id = 1;
$tokens = [];
$this->endpoint
->expects('for_user')
->with($id)
->andThrow(RuntimeException::class);
$result = $this->sut->all_for_user_id($id);
$this->assertSame($tokens, $result);
}
public function testTokensContainCardReturnsTrue()
{
$source = new \stdClass();
$card = new \stdClass();
$source->card = $card;
$token = Mockery::mock(PaymentToken::class);
$tokens = [$token];
$token->shouldReceive('source')->andReturn($source);
$this->assertTrue($this->sut->tokens_contains_card($tokens));
}
public function testTokensContainCardReturnsFalse()
{
$tokens = [];
$this->assertFalse($this->sut->tokens_contains_card($tokens));
}
public function testTokensContainPayPalReturnsTrue()
{
$source = new \stdClass();
$paypal = new \stdClass();
$source->paypal = $paypal;
$token = Mockery::mock(PaymentToken::class);
$tokens = [$token];
$token->shouldReceive('source')->andReturn($source);
$this->assertTrue($this->sut->tokens_contains_paypal($tokens));
}
public function testTokensContainPayPalReturnsFalse()
{
$tokens = [];
$this->assertFalse($this->sut->tokens_contains_paypal($tokens));
}
}