blob: 597e36efcea192878b576acb5807dfa52f3c371d [file] [log] [blame]
swissChilif0cbdc32023-01-05 17:21:38 -05001<?php
2if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4}
5
6/**
7 * Handles and process WC payment tokens API.
8 * Seen in checkout page and my account->add payment method page.
9 *
10 * @since 4.0.0
11 */
12class WC_Stripe_Payment_Tokens {
13 private static $_this;
14
15 /**
16 * Constructor.
17 *
18 * @since 4.0.0
19 * @version 4.0.0
20 */
21 public function __construct() {
22 self::$_this = $this;
23
24 add_filter( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );
25 add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'get_account_saved_payment_methods_list_item_sepa' ], 10, 2 );
26 add_filter( 'woocommerce_get_credit_card_type_label', [ $this, 'normalize_sepa_label' ] );
27 add_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 );
28 add_action( 'woocommerce_payment_token_set_default', [ $this, 'woocommerce_payment_token_set_default' ] );
29 }
30
31 /**
32 * Public access to instance object.
33 *
34 * @since 4.0.0
35 * @version 4.0.0
36 */
37 public static function get_instance() {
38 return self::$_this;
39 }
40
41 /**
42 * Normalizes the SEPA IBAN label on My Account page.
43 *
44 * @since 4.0.0
45 * @version 4.0.0
46 * @param string $label
47 * @return string $label
48 */
49 public function normalize_sepa_label( $label ) {
50 if ( 'sepa iban' === strtolower( $label ) ) {
51 return 'SEPA IBAN';
52 }
53
54 return $label;
55 }
56
57 /**
58 * Extract the payment token from the provided request.
59 *
60 * TODO: Once php requirement is bumped to >= 7.1.0 set return type to ?\WC_Payment_Token
61 * since the return type is nullable, as per
62 * https://www.php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration
63 *
64 * @param array $request Associative array containing payment request information.
65 *
66 * @return \WC_Payment_Token|NULL
67 */
68 public static function get_token_from_request( array $request ) {
69 $payment_method = ! is_null( $request['payment_method'] ) ? $request['payment_method'] : null;
70 $token_request_key = 'wc-' . $payment_method . '-payment-token';
71 if (
72 ! isset( $request[ $token_request_key ] ) ||
73 'new' === $request[ $token_request_key ]
74 ) {
75 return null;
76 }
77
78 //phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
79 $token = \WC_Payment_Tokens::get( wc_clean( $request[ $token_request_key ] ) );
80
81 // If the token doesn't belong to this gateway or the current user it's invalid.
82 if ( ! $token || $payment_method !== $token->get_gateway_id() || $token->get_user_id() !== get_current_user_id() ) {
83 return null;
84 }
85
86 return $token;
87 }
88
89 /**
90 * Checks if customer has saved payment methods.
91 *
92 * @since 4.1.0
93 * @param int $customer_id
94 * @return bool
95 */
96 public static function customer_has_saved_methods( $customer_id ) {
97 $gateways = [ 'stripe', 'stripe_sepa' ];
98
99 if ( empty( $customer_id ) ) {
100 return false;
101 }
102
103 $has_token = false;
104
105 foreach ( $gateways as $gateway ) {
106 $tokens = WC_Payment_Tokens::get_customer_tokens( $customer_id, $gateway );
107
108 if ( ! empty( $tokens ) ) {
109 $has_token = true;
110 break;
111 }
112 }
113
114 return $has_token;
115 }
116
117 /**
118 * Gets saved tokens from Stripe, if they don't already exist in WooCommerce.
119 *
120 * @param array $tokens Array of tokens
121 * @param string $user_id WC User ID
122 * @param string $gateway_id WC Gateway ID
123 *
124 * @return array
125 */
126 public function woocommerce_get_customer_payment_tokens( $tokens, $user_id, $gateway_id ) {
127 if ( WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) {
128 return $this->woocommerce_get_customer_upe_payment_tokens( $tokens, $user_id, $gateway_id );
129 } else {
130 return $this->woocommerce_get_customer_payment_tokens_legacy( $tokens, $user_id, $gateway_id );
131 }
132 }
133
134 /**
135 * Gets saved tokens from Sources API if they don't already exist in WooCommerce.
136 *
137 * @since 3.1.0
138 * @version 4.0.0
139 * @param array $tokens
140 * @return array
141 */
142 public function woocommerce_get_customer_payment_tokens_legacy( $tokens, $customer_id, $gateway_id ) {
143 if ( is_user_logged_in() && class_exists( 'WC_Payment_Token_CC' ) ) {
144 $stored_tokens = [];
145
146 foreach ( $tokens as $token ) {
147 $stored_tokens[ $token->get_token() ] = $token;
148 }
149
150 if ( 'stripe' === $gateway_id ) {
151 $stripe_customer = new WC_Stripe_Customer( $customer_id );
152 $stripe_sources = $stripe_customer->get_sources();
153
154 foreach ( $stripe_sources as $source ) {
155 if ( isset( $source->type ) && 'card' === $source->type ) {
156 if ( ! isset( $stored_tokens[ $source->id ] ) ) {
157 $token = new WC_Payment_Token_CC();
158 $token->set_token( $source->id );
159 $token->set_gateway_id( 'stripe' );
160
161 if ( 'source' === $source->object && 'card' === $source->type ) {
162 $token->set_card_type( strtolower( $source->card->brand ) );
163 $token->set_last4( $source->card->last4 );
164 $token->set_expiry_month( $source->card->exp_month );
165 $token->set_expiry_year( $source->card->exp_year );
166 }
167
168 $token->set_user_id( $customer_id );
169 $token->save();
170 $tokens[ $token->get_id() ] = $token;
171 } else {
172 unset( $stored_tokens[ $source->id ] );
173 }
174 } else {
175 if ( ! isset( $stored_tokens[ $source->id ] ) && 'card' === $source->object ) {
176 $token = new WC_Payment_Token_CC();
177 $token->set_token( $source->id );
178 $token->set_gateway_id( 'stripe' );
179 $token->set_card_type( strtolower( $source->brand ) );
180 $token->set_last4( $source->last4 );
181 $token->set_expiry_month( $source->exp_month );
182 $token->set_expiry_year( $source->exp_year );
183 $token->set_user_id( $customer_id );
184 $token->save();
185 $tokens[ $token->get_id() ] = $token;
186 } else {
187 unset( $stored_tokens[ $source->id ] );
188 }
189 }
190 }
191 }
192
193 if ( 'stripe_sepa' === $gateway_id ) {
194 $stripe_customer = new WC_Stripe_Customer( $customer_id );
195 $stripe_sources = $stripe_customer->get_sources();
196
197 foreach ( $stripe_sources as $source ) {
198 if ( isset( $source->type ) && 'sepa_debit' === $source->type ) {
199 if ( ! isset( $stored_tokens[ $source->id ] ) ) {
200 $token = new WC_Payment_Token_SEPA();
201 $token->set_token( $source->id );
202 $token->set_gateway_id( 'stripe_sepa' );
203 $token->set_last4( $source->sepa_debit->last4 );
204 $token->set_user_id( $customer_id );
205 $token->save();
206 $tokens[ $token->get_id() ] = $token;
207 } else {
208 unset( $stored_tokens[ $source->id ] );
209 }
210 }
211 }
212 }
213 }
214
215 return $tokens;
216 }
217
218 /**
219 * Gets saved tokens from Intentions API if they don't already exist in WooCommerce.
220 *
221 * @param array $tokens Array of tokens
222 * @param string $user_id WC User ID
223 * @param string $gateway_id WC Gateway ID
224 *
225 * @return array
226 */
227 public function woocommerce_get_customer_upe_payment_tokens( $tokens, $user_id, $gateway_id ) {
228 if ( ( ! empty( $gateway_id ) && WC_Stripe_UPE_Payment_Gateway::ID !== $gateway_id ) || ! is_user_logged_in() ) {
229 return $tokens;
230 }
231
232 if ( count( $tokens ) >= get_option( 'posts_per_page' ) ) {
233 // The tokens data store is not paginated and only the first "post_per_page" (defaults to 10) tokens are retrieved.
234 // Having 10 saved credit cards is considered an unsupported edge case, new ones that have been stored in Stripe won't be added.
235 return $tokens;
236 }
237
238 $gateway = new WC_Stripe_UPE_Payment_Gateway();
239 $reusable_payment_methods = array_filter( $gateway->get_upe_enabled_payment_method_ids(), [ $gateway, 'is_enabled_for_saved_payments' ] );
240 $customer = new WC_Stripe_Customer( $user_id );
241 $remaining_tokens = [];
242
243 foreach ( $tokens as $token ) {
244 if ( WC_Stripe_UPE_Payment_Gateway::ID === $token->get_gateway_id() ) {
245 $payment_method_type = $this->get_payment_method_type_from_token( $token );
246 if ( ! in_array( $payment_method_type, $reusable_payment_methods, true ) ) {
247 // Remove saved token from list, if payment method is not enabled.
248 unset( $tokens[ $token->get_id() ] );
249 } else {
250 // Store relevant existing tokens here.
251 // We will use this list to check whether these methods still exist on Stripe's side.
252 $remaining_tokens[ $token->get_token() ] = $token;
253 }
254 }
255 }
256
257 $retrievable_payment_method_types = [];
258 foreach ( $reusable_payment_methods as $payment_method_id ) {
259 $upe_payment_method = $gateway->payment_methods[ $payment_method_id ];
260 if ( ! in_array( $upe_payment_method->get_retrievable_type(), $retrievable_payment_method_types, true ) ) {
261 $retrievable_payment_method_types[] = $upe_payment_method->get_retrievable_type();
262 }
263 }
264
265 foreach ( $retrievable_payment_method_types as $payment_method_id ) {
266 $payment_methods = $customer->get_payment_methods( $payment_method_id );
267
268 // Prevent unnecessary recursion, WC_Payment_Token::save() ends up calling 'woocommerce_get_customer_payment_tokens' in some cases.
269 remove_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );
270 foreach ( $payment_methods as $payment_method ) {
271 if ( ! isset( $remaining_tokens[ $payment_method->id ] ) ) {
272 $payment_method_type = $this->get_original_payment_method_type( $payment_method );
273 if ( ! in_array( $payment_method_type, $reusable_payment_methods, true ) ) {
274 continue;
275 }
276 // Create new token for new payment method and add to list.
277 $upe_payment_method = $gateway->payment_methods[ $payment_method_type ];
278 $token = $upe_payment_method->create_payment_token_for_user( $user_id, $payment_method );
279 $tokens[ $token->get_id() ] = $token;
280 } else {
281 // Count that existing token for payment method is still present on Stripe.
282 // Remaining IDs in $remaining_tokens no longer exist with Stripe and will be eliminated.
283 unset( $remaining_tokens[ $payment_method->id ] );
284 }
285 }
286 add_action( 'woocommerce_get_customer_payment_tokens', [ $this, 'woocommerce_get_customer_payment_tokens' ], 10, 3 );
287 }
288
289 // Eliminate remaining payment methods no longer known by Stripe.
290 // Prevent unnecessary recursion, when deleting tokens.
291 remove_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 );
292 foreach ( $remaining_tokens as $token ) {
293 unset( $tokens[ $token->get_id() ] );
294 $token->delete();
295 }
296 add_action( 'woocommerce_payment_token_deleted', [ $this, 'woocommerce_payment_token_deleted' ], 10, 2 );
297
298 return $tokens;
299 }
300
301 /**
302 * Returns original type of payment method from Stripe payment method response,
303 * after checking whether payment method is SEPA method generated from another type.
304 *
305 * @param object $payment_method Stripe payment method JSON object.
306 *
307 * @return string Payment method type/ID
308 */
309 private function get_original_payment_method_type( $payment_method ) {
310 if ( WC_Stripe_UPE_Payment_Method_Sepa::STRIPE_ID === $payment_method->type ) {
311 if ( ! is_null( $payment_method->sepa_debit->generated_from->charge ) ) {
312 return $payment_method->sepa_debit->generated_from->charge->payment_method_details->type;
313 }
314 if ( ! is_null( $payment_method->sepa_debit->generated_from->setup_attempt ) ) {
315 return $payment_method->sepa_debit->generated_from->setup_attempt->payment_method_details->type;
316 }
317 }
318 return $payment_method->type;
319 }
320
321 /**
322 * Returns original Stripe payment method type from payment token
323 *
324 * @param WC_Payment_Token $payment_token WC Payment Token (CC or SEPA)
325 *
326 * @return string
327 */
328 private function get_payment_method_type_from_token( $payment_token ) {
329 $type = $payment_token->get_type();
330 if ( 'CC' === $type ) {
331 return 'card';
332 } elseif ( 'sepa' === $type ) {
333 return $payment_token->get_payment_method_type();
334 } else {
335 return $type;
336 }
337 }
338
339 /**
340 * Controls the output for SEPA on the my account page.
341 *
342 * @since 4.0.0
343 * @version 4.0.0
344 * @param array $item Individual list item from woocommerce_saved_payment_methods_list
345 * @param WC_Payment_Token $payment_token The payment token associated with this method entry
346 * @return array Filtered item
347 */
348 public function get_account_saved_payment_methods_list_item_sepa( $item, $payment_token ) {
349 if ( 'sepa' === strtolower( $payment_token->get_type() ) ) {
350 $item['method']['last4'] = $payment_token->get_last4();
351 $item['method']['brand'] = esc_html__( 'SEPA IBAN', 'woocommerce-gateway-stripe' );
352 }
353
354 return $item;
355 }
356
357 /**
358 * Delete token from Stripe.
359 *
360 * @since 3.1.0
361 * @version 4.0.0
362 */
363 public function woocommerce_payment_token_deleted( $token_id, $token ) {
364 $stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
365 if ( WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) {
366 if ( WC_Stripe_UPE_Payment_Gateway::ID === $token->get_gateway_id() ) {
367 $stripe_customer->detach_payment_method( $token->get_token() );
368 }
369 } else {
370 if ( 'stripe' === $token->get_gateway_id() || 'stripe_sepa' === $token->get_gateway_id() ) {
371 $stripe_customer->delete_source( $token->get_token() );
372 }
373 }
374 }
375
376 /**
377 * Set as default in Stripe.
378 *
379 * @since 3.1.0
380 * @version 4.0.0
381 */
382 public function woocommerce_payment_token_set_default( $token_id ) {
383 $token = WC_Payment_Tokens::get( $token_id );
384 $stripe_customer = new WC_Stripe_Customer( get_current_user_id() );
385
386 if ( WC_Stripe_Feature_Flags::is_upe_checkout_enabled() ) {
387 if ( WC_Stripe_UPE_Payment_Gateway::ID === $token->get_gateway_id() ) {
388 $stripe_customer->set_default_payment_method( $token->get_token() );
389 }
390 } else {
391 if ( 'stripe' === $token->get_gateway_id() || 'stripe_sepa' === $token->get_gateway_id() ) {
392 $stripe_customer->set_default_source( $token->get_token() );
393 }
394 }
395 }
396}
397
398new WC_Stripe_Payment_Tokens();