diff --git a/Extension.php b/Extension.php index a2d8578..2a81231 100644 --- a/Extension.php +++ b/Extension.php @@ -33,6 +33,11 @@ public function registerPaymentGateways() 'name' => 'lang:igniter.payregister::default.stripe.text_payment_title', 'description' => 'lang:igniter.payregister::default.stripe.text_payment_desc', ], + \Igniter\PayRegister\Payments\StripeCheckout::class => [ + 'code' => 'stripecheckout', + 'name' => 'lang:igniter.payregister::default.stripecheckout.text_payment_title', + 'description' => 'lang:igniter.payregister::default.stripecheckout.text_payment_desc', + ], \Igniter\PayRegister\Payments\Mollie::class => [ 'code' => 'mollie', 'name' => 'lang:igniter.payregister::default.mollie.text_payment_title', diff --git a/language/en/default.php b/language/en/default.php index ab8db32..6efa310 100644 --- a/language/en/default.php +++ b/language/en/default.php @@ -123,6 +123,35 @@ 'help_locale_code' => 'See Stripe.js supported locales [ + 'text_tab_general' => 'General', + 'text_payment_title' => 'Stripe Checkout', + 'text_payment_desc' => 'Accept credit card, Apple Pay, Google Pay using Stripe builtin Checkout page', + 'text_credit_or_debit' => 'Credit or debit card', + + 'text_auth_only' => 'Authorization Only', + 'text_auth_capture' => 'Authorization & Capture', + 'text_description' => 'Pay by Credit Card using Stripe', + 'text_live' => 'Live', + 'text_test' => 'Test', + 'text_stripe_charge_description' => '%s Charge for %s', + 'text_payment_status' => 'Payment %s (%s)', + + 'label_title' => 'Title', + 'label_description' => 'Description', + 'label_transaction_mode' => 'Transaction Mode', + 'label_transaction_type' => 'Transaction Type', + 'label_test_secret_key' => 'Test Secret Key', + 'label_test_publishable_key' => 'Test Publishable Key', + 'label_live_secret_key' => 'Live Secret Key', + 'label_live_publishable_key' => 'Live Publishable Key', + 'label_locale_code' => 'Locale Code', + 'label_priority' => 'Priority', + 'label_status' => 'Status', + + 'help_locale_code' => 'See Stripe.js supported locales [ 'text_payment_title' => 'Mollie Payment', 'text_payment_desc' => 'Accept credit card payments using Mollie API', diff --git a/payments/Stripe.php b/payments/Stripe.php index f7ddebb..048ee75 100644 --- a/payments/Stripe.php +++ b/payments/Stripe.php @@ -23,7 +23,7 @@ class Stripe extends BasePaymentGateway public function registerEntryPoints() { return [ - 'stripe_webhook' => 'processWebhookUrl', + // 'stripe_webhook' => 'processWebhookUrl', ]; } diff --git a/payments/StripeCheckout.php b/payments/StripeCheckout.php new file mode 100644 index 0000000..628e3aa --- /dev/null +++ b/payments/StripeCheckout.php @@ -0,0 +1,226 @@ + 'processWebhookUrl', + 'stripe_checkout_return_url' => 'processSuccessUrl', + 'stripe_checkout_cancel_url' => 'processCancelUrl', + ]; + } + + public function isTestMode() + { + return $this->model->transaction_mode != 'live'; + } + + public function getPublishableKey() + { + return $this->isTestMode() ? $this->model->test_publishable_key : $this->model->live_publishable_key; + } + + public function getSecretKey() + { + return $this->isTestMode() ? $this->model->test_secret_key : $this->model->live_secret_key; + } + + public function shouldAuthorizePayment() + { + return $this->model->transaction_type == 'auth_only'; + } + + public function isApplicable($total, $host) + { + return $host->order_total <= $total; + } + + /** + * @param array $data + * @param \Admin\Models\Payments_model $host + * @param \Admin\Models\Orders_model $order + * + * @return mixed + */ + public function processPaymentForm($data, $host, $order) + { + $this->validatePaymentMethod($order, $host); + + $fields = $this->getPaymentFormFields($order, $data); + + try { + $gateway = $this->createGateway(); + $response = $gateway->checkout->sessions->create($fields); + + return Redirect::to($response->url); + + } catch (Exception $ex) { + $order->logPaymentAttempt('Payment error -> ' . $ex->getMessage(), 0, $fields, []); + throw new ApplicationException('Sorry, there was an error processing your payment. Please try again later.'); + } + } + + public function processSuccessUrl($params) + { + $hash = $params[0] ?? null; + $redirectPage = input('redirect'); + $cancelPage = input('cancel'); + + $order = $this->createOrderModel()->whereHash($hash)->first(); + + try { + if (!$hash || !$order instanceof Orders_model) + throw new ApplicationException('No order found'); + + if (!strlen($redirectPage)) + throw new ApplicationException('No redirect page found'); + + if (!strlen($cancelPage)) + throw new ApplicationException('No cancel page found'); + + $paymentMethod = $order->payment_method; + if (!$paymentMethod || $paymentMethod->getGatewayClass() != static::class) + throw new ApplicationException('No valid payment method found'); + + $order->logPaymentAttempt('Payment successful', 1, [], $paymentMethod, true); + $order->updateOrderStatus($paymentMethod->order_status, ['notify' => false]); + $order->markAsPaymentProcessed(); + + return Redirect::to(page_url($redirectPage, [ + 'id' => $order->getKey(), + 'hash' => $order->hash, + ])); + } catch (Exception $ex) { + $order->logPaymentAttempt('Payment error -> ' . $ex->getMessage(), 0, [], []); + flash()->warning($ex->getMessage())->important(); + } + + return Redirect::to(page_url($cancelPage)); + } + + public function processCancelUrl($params) + { + $hash = $params[0] ?? null; + $order = $this->createOrderModel()->whereHash($hash)->first(); + if (!$hash || !$order instanceof Orders_model) + throw new ApplicationException('No order found'); + + if (!strlen($redirectPage = input('redirect'))) + throw new ApplicationException('No redirect page found'); + + $paymentMethod = $order->payment_method; + if (!$paymentMethod || $paymentMethod->getGatewayClass() != static::class) + throw new ApplicationException('No valid payment method found'); + + $order->logPaymentAttempt('Payment canceled by customer', 0, input()); + + return Redirect::to(page_url($redirectPage)); + } + + protected function createGateway() + { + \Stripe\Stripe::setAppInfo( + 'TastyIgniter Stripe', + '1.0.0', + 'https://tastyigniter.com/marketplace/item/igniter-payregister', + 'pp_partner_JZyCCGR3cOwj9S' // Used by Stripe to identify this integration + ); + + $stripeClient = new StripeClient([ + 'api_key' => $this->getSecretKey(), + ]); + + $this->fireSystemEvent('payregister.stripe.extendClient', [$stripeClient]); + + return $stripeClient; + } + + protected function getPaymentFormFields($order, $data = []) + { + $cancelUrl = $this->makeEntryPointUrl('stripe_checkout_cancel_url') . '/' . $order->hash; + $successUrl = $this->makeEntryPointUrl('stripe_checkout_return_url') . '/' . $order->hash; + $successUrl .= '?redirect=' . array_get($data, 'successPage') . '&cancel=' . array_get($data, 'cancelPage'); + + $fields = [ + 'line_items' => [ + [ + 'price_data' => [ + 'currency' => currency()->getUserCurrency(), + // All amounts sent to Stripe must be in integers, representing the lowest currency unit (cents) + 'unit_amount_decimal' => number_format($order->order_total, 2, '.', '')*100, + 'product_data' => [ + 'name' => "Test", + ], + ], + 'quantity' => 1, + ], + ], + 'cancel_url' => $cancelUrl . '?redirect=' . array_get($data, 'cancelPage'), + 'success_url' => $successUrl, + 'mode' => 'payment', + 'metadata' => [ + 'order_id' => $order->order_id, + ], + ]; + + $this->fireSystemEvent('payregister.stripecheckout.extendFields', [&$fields, $order, $data]); + + return $fields; + } + + + public function processWebhookUrl() + { + if (strtolower(request()->method()) !== 'post') + return response('Request method must be POST', 400); + + $payload = json_decode(request()->getContent(), true); + if (!isset($payload['type']) || !strlen($eventType = $payload['type'])) + return response('Missing webhook event name', 400); + + $eventName = Str::studly(str_replace('.', '_', $eventType)); + $methodName = 'webhookHandle'.$eventName; + + if (method_exists($this, $methodName)) + $this->$methodName($payload); + + Event::fire('payregister.stripecheckout.webhook.handle'.$eventName, [$payload]); + + return response('Webhook Handled'); + } + + protected function webhookHandleCheckoutSessionCompleted($payload) + { + if ($order = Orders_model::find($payload['data']['object']['metadata']['order_id'])) { + if (!$order->isPaymentProcessed()) { + if ($payload['data']['object']['status'] === 'requires_capture') { + $order->logPaymentAttempt('Payment authorized', 1, [], $payload['data']['object']); + } + else { + $order->logPaymentAttempt('Payment successful', 1, [], $payload['data']['object'], true); + } + + $order->updateOrderStatus($this->model->order_status, ['notify' => false]); + $order->markAsPaymentProcessed(); + } + } + } +} diff --git a/payments/stripecheckout/fields.php b/payments/stripecheckout/fields.php new file mode 100644 index 0000000..55d0fbf --- /dev/null +++ b/payments/stripecheckout/fields.php @@ -0,0 +1,118 @@ + [ + 'setup' => [ + 'type' => 'partial', + 'path' => '$/igniter/payregister/payments/stripecheckout/info', + ], + 'transaction_mode' => [ + 'label' => 'lang:igniter.payregister::default.stripecheckout.label_transaction_mode', + 'type' => 'radiotoggle', + 'default' => 'test', + 'span' => 'left', + 'options' => [ + 'live' => 'lang:igniter.payregister::default.stripecheckout.text_live', + 'test' => 'lang:igniter.payregister::default.stripecheckout.text_test', + ], + ], + 'transaction_type' => [ + 'label' => 'lang:igniter.payregister::default.stripecheckout.label_transaction_type', + 'type' => 'radiotoggle', + 'default' => 'auth_capture', + 'span' => 'right', + 'options' => [ + 'auth_capture' => 'lang:igniter.payregister::default.stripecheckout.text_auth_capture', + 'auth_only' => 'lang:igniter.payregister::default.stripecheckout.text_auth_only', + ], + ], + 'live_secret_key' => [ + 'label' => 'lang:igniter.payregister::default.stripecheckout.label_live_secret_key', + 'type' => 'text', + 'span' => 'left', + 'trigger' => [ + 'action' => 'show', + 'field' => 'transaction_mode', + 'condition' => 'value[live]', + ], + ], + 'live_publishable_key' => [ + 'label' => 'lang:igniter.payregister::default.stripecheckout.label_live_publishable_key', + 'type' => 'text', + 'span' => 'right', + 'trigger' => [ + 'action' => 'show', + 'field' => 'transaction_mode', + 'condition' => 'value[live]', + ], + ], + 'test_secret_key' => [ + 'label' => 'lang:igniter.payregister::default.stripecheckout.label_test_secret_key', + 'type' => 'text', + 'span' => 'left', + 'trigger' => [ + 'action' => 'show', + 'field' => 'transaction_mode', + 'condition' => 'value[test]', + ], + ], + 'test_publishable_key' => [ + 'label' => 'lang:igniter.payregister::default.stripecheckout.label_test_publishable_key', + 'type' => 'text', + 'span' => 'right', + 'trigger' => [ + 'action' => 'show', + 'field' => 'transaction_mode', + 'condition' => 'value[test]', + ], + ], + 'locale_code' => [ + 'label' => 'lang:igniter.payregister::default.stripecheckout.label_locale_code', + 'type' => 'text', + 'span' => 'left', + ], + 'order_fee_type' => [ + 'label' => 'lang:igniter.payregister::default.label_order_fee_type', + 'type' => 'radiotoggle', + 'span' => 'right', + 'cssClass' => 'flex-width', + 'default' => 1, + 'options' => [ + 1 => 'lang:admin::lang.menus.text_fixed_amount', + 2 => 'lang:admin::lang.menus.text_percentage', + ], + ], + 'order_fee' => [ + 'label' => 'lang:igniter.payregister::default.label_order_fee', + 'type' => 'currency', + 'span' => 'right', + 'cssClass' => 'flex-width', + 'default' => 0, + 'comment' => 'lang:igniter.payregister::default.help_order_fee', + ], + 'order_total' => [ + 'label' => 'lang:igniter.payregister::default.label_order_total', + 'type' => 'currency', + 'span' => 'left', + 'comment' => 'lang:igniter.payregister::default.help_order_total', + ], + 'order_status' => [ + 'label' => 'lang:igniter.payregister::default.label_order_status', + 'type' => 'select', + 'options' => [\Admin\Models\Statuses_model::class, 'getDropdownOptionsForOrder'], + 'span' => 'right', + 'comment' => 'lang:igniter.payregister::default.help_order_status', + ], + ], + 'rules' => [ + ['transaction_mode', 'lang:igniter.payregister::default.stripecheckout.label_transaction_mode', 'string'], + ['live_secret_key', 'lang:igniter.payregister::default.stripecheckout.label_live_secret_key', 'string'], + ['live_publishable_key', 'lang:igniter.payregister::default.stripecheckout.label_live_publishable_key', 'string'], + ['test_secret_key', 'lang:igniter.payregister::default.stripecheckout.label_test_secret_key', 'string'], + ['test_publishable_key', 'lang:igniter.payregister::default.stripecheckout.label_test_publishable_key', 'string'], + ['order_fee_type', 'lang:igniter.payregister::default.label_order_fee_type', 'integer'], + ['order_fee', 'lang:igniter.payregister::default.label_order_fee', 'numeric'], + ['order_total', 'lang:igniter.payregister::default.label_order_total', 'numeric'], + ['order_status', 'lang:igniter.payregister::default.label_order_status', 'integer'], + ], +]; diff --git a/payments/stripecheckout/info.blade.php b/payments/stripecheckout/info.blade.php new file mode 100644 index 0000000..467a858 --- /dev/null +++ b/payments/stripecheckout/info.blade.php @@ -0,0 +1,11 @@ +
+
Configure Webhook
+
+ You can configure the webhook url + + in your Stripe Dashboard > Developers > Webhooks +
+
diff --git a/payments/stripecheckout/payment_form.blade.php b/payments/stripecheckout/payment_form.blade.php new file mode 100644 index 0000000..e69de29