PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.1
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
fluent-cart / app / Modules / PaymentMethods / AirwallexGateway / Airwallex.php

Airwallex.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.1, at app/Modules/PaymentMethods/AirwallexGateway/Airwallex.php

482 lines 16.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Modules\PaymentMethods\AirwallexGateway;
4
5 use FluentCart\App\App;
6 use FluentCart\App\Modules\PaymentMethods\Core\AbstractPaymentGateway;
7 use FluentCart\App\Services\Payments\PaymentInstance;
8 use FluentCart\App\Vite;
9 use FluentCart\Framework\Support\Arr;
10
11 class Airwallex extends AbstractPaymentGateway
12 {
13 public array $supportedFeatures = [
14 'payment',
15 'refund',
16 'webhook'
17 ];
18
19 public function __construct()
20 {
21 parent::__construct(new AirwallexSettingsBase());
22 }
23
24 public function meta(): array
25 {
26 return [
27 'title' => __('Airwallex', 'fluent-cart'),
28 'route' => 'airwallex',
29 'description' => __('Pay securely with Airwallex - Global payment processing', 'fluent-cart'),
30 'logo' => Vite::getAssetUrl("images/payment-methods/airwallex-logo.svg"),
31 'logo_light' => Vite::getAssetUrl("images/payment-methods/airwallex-logo-light.svg"),
32 'icon' => Vite::getAssetUrl("images/payment-methods/airwallex-logo.svg"),
33 'brand_color' => '#6c5ce7',
34 'status' => $this->settings->get('is_active') === 'yes',
35 'upcoming' => true,
36 ];
37 }
38
39 public function boot()
40 {
41 // init IPN related class/actions here
42 add_filter('fluent_cart/payment_methods/airwallex_settings', [$this, 'getSettings'], 10, 2);
43 }
44
45 public function makePaymentFromPaymentInstance(PaymentInstance $paymentInstance)
46 {
47 // todo: implement in future
48 die();
49 }
50
51 public function refund($refundInfo, $order, $transaction)
52 {
53 // todo: implement in future
54 die();
55 }
56
57 public function handleIPN(): void
58 {
59 $payload = json_decode(file_get_contents('php://input'), true); // will get from request after verification
60 // Verify webhook signature
61 if (!$this->verifyWebhookSignature($payload)) {
62 http_response_code(401);
63 exit('Unauthorized');
64 }
65
66 $eventType = $payload['name'] ?? '';
67
68 switch ($eventType) {
69 case 'payment_intent.succeeded':
70 $this->handlePaymentSucceeded($payload);
71 break;
72 case 'payment_intent.failed':
73 $this->handlePaymentFailed($payload);
74 break;
75 case 'refund.received':
76 $this->handleRefundReceived($payload);
77 break;
78 }
79
80 http_response_code(200);
81 exit('OK');
82 }
83
84 public function getOrderInfo(array $data)
85 {
86 $items = $this->getCheckoutItems();
87
88 $subTotal = 0;
89 foreach ($items as $item) {
90 $subTotal += intval($item['quantity'] * $item['unit_price']);
91 }
92
93 $paymentArgs = [
94 'client_id' => $this->settings->getClientId(),
95 'amount' => $subTotal,
96 'currency' => $this->storeSettings->get('currency'),
97 ];
98
99 wp_send_json([
100 'status' => 'success',
101 'payment_args' => $paymentArgs,
102 'has_subscription' => false
103 ], 200);
104 }
105
106 public function fields()
107 {
108 $webhook_url = trailingslashit(site_url()) . '?fct_payment_listener=1&method=airwallex';
109 $webhook_instructions = sprintf(
110 '<div>
111 <p><b>%1$s</b><code class="copyable-content">%2$s</code></p>
112 <p>%3$s</p>
113 <br>
114 <h4>%4$s</h4>
115 <br>
116 <p>%5$s</p>
117 <p>%6$s <a href="https://www.airwallex.com/app/settings/developer" target="_blank">%7$s</a></p>
118 <p>%8$s <code class="copyable-content">%2$s</code></p>
119 <p>%9$s</p>
120 </div>',
121 __('Webhook URL: ', 'fluent-cart'), // %1$s
122 $webhook_url, // %2$s (reused)
123 __('You should configure your Airwallex webhooks to get all updates of your payments remotely.', 'fluent-cart'), // %3$s
124 __('How to configure?', 'fluent-cart'), // %4$s
125 __('In your Airwallex Console:', 'fluent-cart'), // %5$s
126 __('Go to Developers > Webhooks >', 'fluent-cart'), // %6$s
127 __('Add webhook', 'fluent-cart'), // %7$s
128 __('Enter The Webhook URL: ', 'fluent-cart'), // %8$s
129 __('Select payment events', 'fluent-cart') // %9$s
130 );
131
132 return array(
133 // 'notice' => [
134 // 'value' => $this->getStoreModeNotice(),
135 // 'label' => __('Store Mode notice', 'fluent-cart'),
136 // 'type' => 'notice'
137 // ],
138 'upcoming' => [
139 'value' => $this->isUpcoming(),
140 'label' => __('Payment method is upcoming!', 'fluent-cart'),
141 'type' => 'upcoming'
142 ],
143 'payment_mode' => [
144 'type' => 'tabs',
145 'schema' => [
146 [
147 'type' => 'tab',
148 'label' => __('Test credentials', 'fluent-cart'),
149 'value' => 'test',
150 'schema' => [
151 'test_client_id' => array(
152 'value' => '',
153 'label' => __('Test Client ID', 'fluent-cart'),
154 'type' => 'text',
155 'placeholder' => __('Your test client ID', 'fluent-cart'),
156 'dependency' => [
157 'depends_on' => 'payment_mode',
158 'operator' => '=',
159 'value' => 'test'
160 ]
161 ),
162 'test_api_key' => array(
163 'value' => '',
164 'label' => __('Test API Key', 'fluent-cart'),
165 'type' => 'password',
166 'placeholder' => __('Your test API key', 'fluent-cart'),
167 'dependency' => [
168 'depends_on' => 'payment_mode',
169 'operator' => '=',
170 'value' => 'test'
171 ]
172 ),
173 ],
174 ],
175 [
176 'type' => 'tab',
177 'label' => __('Live credentials', 'fluent-cart'),
178 'value' => 'live',
179 'schema' => [
180 'live_client_id' => array(
181 'value' => '',
182 'label' => __('Live Client ID', 'fluent-cart'),
183 'type' => 'text',
184 'placeholder' => __('Your live client ID', 'fluent-cart'),
185 'dependency' => [
186 'depends_on' => 'payment_mode',
187 'operator' => '=',
188 'value' => 'live'
189 ]
190 ),
191 'live_api_key' => array(
192 'value' => '',
193 'label' => __('Live API Key', 'fluent-cart'),
194 'type' => 'password',
195 'placeholder' => __('Your live API key', 'fluent-cart'),
196 'dependency' => [
197 'depends_on' => 'payment_mode',
198 'operator' => '=',
199 'value' => 'live'
200 ]
201 ),
202 ]
203 ]
204 ]
205 ],
206 'webhook_desc' => array(
207 'value' => $webhook_instructions,
208 'label' => __('Webhook URL', 'fluent-cart'),
209 'type' => 'html_attr'
210 ),
211 );
212 }
213
214 public static function validateSettings($data): array
215 {
216 $clientId = $data['client_id'] ?? '';
217 $apiKey = $data['api_key'] ?? '';
218
219 if (empty($clientId) || empty($apiKey)) {
220 return [
221 'status' => 'failed',
222 'message' => __('Client ID and API Key are required', 'fluent-cart')
223 ];
224 }
225
226 try {
227 $testResponse = static::testApiConnection($clientId, $apiKey, $data['payment_mode'] ?? 'demo');
228
229 return [
230 'status' => 'success',
231 'message' => __('Airwallex settings validated successfully', 'fluent-cart')
232 ];
233 } catch (\Exception $e) {
234 return [
235 'status' => 'failed',
236 'message' => $e->getMessage()
237 ];
238 }
239 }
240
241 public function getEnqueueScriptSrc($hasSubscription = 'no'): array
242 {
243 $mode = $this->settings->getMode();
244 $airwallexJsUrl = $mode === 'production'
245 ? 'https://checkout.airwallex.com/assets/elements.bundle.min.js'
246 : 'https://checkout.airwallex.com/assets/elements.bundle.min.js';
247
248 return [
249 [
250 'handle' => 'airwallex-elements-js',
251 'src' => $airwallexJsUrl,
252 ],
253 [
254 'handle' => 'fluent-cart-airwallex-checkout',
255 'src' => Vite::getEnqueuePath('public/payment-methods/airwallex-checkout.js'),
256 'deps' => ['airwallex-elements-js']
257 ]
258 ];
259 }
260
261 public function getEnqueueStyleSrc(): array
262 {
263 return [
264 [
265 'handle' => 'fluent-cart-airwallex-styles',
266 'src' => Vite::getEnqueuePath('public/payment-methods/airwallex.css'),
267 ]
268 ];
269 }
270
271 private function getAccessToken()
272 {
273 $clientId = $this->settings->getClientId();
274 $apiKey = $this->settings->getApiKey();
275 $baseUrl = $this->getApiBaseUrl();
276
277 $response = wp_remote_post("{$baseUrl}/api/v1/authentication/login", [
278 'headers' => [
279 'Content-Type' => 'application/json'
280 ],
281 'body' => json_encode([
282 'x-client-id' => $clientId,
283 'x-api-key' => $apiKey
284 ]),
285 'timeout' => 30
286 ]);
287
288 if (is_wp_error($response)) {
289 throw new \Exception(esc_html($response->get_error_message()));
290 }
291
292 $body = wp_remote_retrieve_body($response);
293 $data = json_decode($body, true);
294
295 if (wp_remote_retrieve_response_code($response) !== 201) {
296 throw new \Exception(esc_html__('Failed to authenticate with Airwallex', 'fluent-cart'));
297 }
298
299 return $data['token'];
300 }
301
302 private function createAirwallexPaymentIntent($data, $accessToken)
303 {
304 $baseUrl = $this->getApiBaseUrl();
305
306 $response = wp_remote_post("{$baseUrl}/api/v1/pa/payment_intents/create", [
307 'headers' => [
308 'Authorization' => 'Bearer ' . $accessToken,
309 'Content-Type' => 'application/json'
310 ],
311 'body' => json_encode($data),
312 'timeout' => 30
313 ]);
314
315 if (is_wp_error($response)) {
316 throw new \Exception(esc_html($response->get_error_message()));
317 }
318
319 $body = wp_remote_retrieve_body($response);
320 $responseData = json_decode($body, true);
321
322 if (wp_remote_retrieve_response_code($response) !== 201) {
323 $error = $responseData['message'] ?? 'Unknown error';
324 throw new \Exception(esc_html($error));
325 }
326
327 return $responseData;
328 }
329
330 private function processAirwallexRefund($refundData, $accessToken)
331 {
332 $baseUrl = $this->getApiBaseUrl();
333
334 $response = wp_remote_post("{$baseUrl}/api/v1/pa/refunds/create", [
335 'headers' => [
336 'Authorization' => 'Bearer ' . $accessToken,
337 'Content-Type' => 'application/json'
338 ],
339 'body' => json_encode($refundData),
340 'timeout' => 30
341 ]);
342
343 if (is_wp_error($response)) {
344 throw new \Exception(esc_html($response->get_error_message()));
345 }
346
347 $body = wp_remote_retrieve_body($response);
348 $responseData = json_decode($body, true);
349
350 if (wp_remote_retrieve_response_code($response) !== 201) {
351 $error = $responseData['message'] ?? 'Unknown error';
352 throw new \Exception(esc_html($error));
353 }
354
355 return $responseData;
356 }
357
358 private static function testApiConnection($clientId, $apiKey, $mode)
359 {
360 $baseUrl = $mode === 'production'
361 ? 'https://api.airwallex.com'
362 : 'https://api-demo.airwallex.com';
363
364 $response = wp_remote_post("{$baseUrl}/api/v1/authentication/login", [
365 'headers' => [
366 'Content-Type' => 'application/json'
367 ],
368 'body' => json_encode([
369 'x-client-id' => $clientId,
370 'x-api-key' => $apiKey
371 ]),
372 'timeout' => 30
373 ]);
374
375 if (is_wp_error($response)) {
376 throw new \Exception(esc_html($response->get_error_message()));
377 }
378
379 if (wp_remote_retrieve_response_code($response) !== 201) {
380 throw new \Exception(esc_html__('Invalid Airwallex credentials', 'fluent-cart'));
381 }
382
383 return true;
384 }
385
386 private function verifyWebhookSignature($payload)
387 {
388 $serverData = App::request()->server();
389 $signature = Arr::get($serverData, 'HTTP_X_SIGNATURE', '');
390 $timestamp = Arr::get($serverData, 'HTTP_X_TIMESTAMP', '');
391 $webhookSecret = $this->settings->getWebhookSecret();
392
393 if (!$signature || !$timestamp || !$webhookSecret) {
394 return false;
395 }
396
397 $signedPayload = $timestamp . json_encode($payload);
398 $expectedSignature = hash_hmac('sha256', $signedPayload, $webhookSecret);
399
400 return hash_equals($expectedSignature, $signature);
401 }
402
403 private function getApiBaseUrl()
404 {
405 return $this->settings->getMode() === 'production'
406 ? 'https://api.airwallex.com'
407 : 'https://api-demo.airwallex.com';
408 }
409
410 private function getWebhookUrl()
411 {
412 return site_url('?fluent_cart_payment_api_notify=airwallex');
413 }
414
415 public function webHookPaymentMethodName()
416 {
417 return $this->getMeta('route');
418 }
419
420 private function generateRequestId($order, $type = 'payment')
421 {
422 return $order->uuid . '_' . $type . '_' . time();
423 }
424
425 private function handlePaymentSucceeded($payload)
426 {
427 $paymentIntent = $payload['data']['object'] ?? [];
428 $orderId = $paymentIntent['metadata']['order_id'] ?? '';
429
430 if ($orderId) {
431 $order = fluent_cart_get_order($orderId);
432 if ($order) {
433 $this->handlePaymentSuccess($paymentIntent, $order);
434 }
435 }
436 }
437
438 private function handlePaymentFailed($payload)
439 {
440 $paymentIntent = $payload['data']['object'] ?? [];
441 $orderId = $paymentIntent['metadata']['order_id'] ?? '';
442
443 if ($orderId) {
444 $order = fluent_cart_get_order($orderId);
445 if ($order) {
446 $this->handlePaymentFailure($paymentIntent, $order);
447 }
448 }
449 }
450
451 private function handleRefundReceived($payload)
452 {
453 $refund = $payload['data']['object'] ?? [];
454 // Process refund webhook logic here
455 }
456
457 private function handlePaymentSuccess($paymentIntent, $order)
458 {
459 $order->payment_status = 'paid';
460 $order->status = 'processing';
461 $order->vendor_charge_id = $paymentIntent['id'];
462 $order->save();
463
464 do_action('fluent_cart/payment_success', [
465 'order' => $order,
466 'payment_intent' => $paymentIntent
467 ]);
468 }
469
470 private function handlePaymentFailure($paymentIntent, $order)
471 {
472 $order->payment_status = 'failed';
473 $order->status = 'failed';
474 $order->save();
475
476 do_action('fluent_cart/payment_failed', [
477 'order' => $order,
478 'payment_intent' => $paymentIntent
479 ]);
480 }
481 }
482