How do I succesfully install Adyen Web Drop In on my website? - php

I'm new to programming and I am currently doing an internship to improve. That being said, the last days I've spend over 15 hours trying to get Adyen to work on my webpage (for testing purposes), using a test account.
I've first struggled alot with authorization errors, having refilled API Keys, Client Keys, Origin Keys, HMAC Keys and Account Names hundreds of times. Rereading guides over and over without results, nothing worked. Then suddenly, without changing a thing, it worked using curl in terminal to test a first test payment.
I followed the guide: https://docs.adyen.com/online-payments/drop-in-web?tab=redirect_payment_method_1 (Script method), but started getting errors at step 3 (https://gyazo.com/ed4b386035c681e433b0c3616ed90f97). After spending hours without getting a step closer I started creating comments for every step and just copy/pasting everything. Filling in all keys ect. to see if it would get solved by a later step in the code.
I've replaced my account information and keys with 'xxx':
<!-- Adyen CSS, Step 2 -->
<link rel="stylesheet" href="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/3.21.0/adyen.css"
integrity="sha384-Qg5+SF5siQdYOd98ZWyvD7nx35CSLtRdiqlLUQBB5gBSLh45T8kkgiDUgCAsMGkt"
crossorigin="anonymous">
<!-- Adyen provides the SRI hash that you include as the integrity attribute. Refer to our release notes to get the SRI hash for the specific version. https://docs.adyen.com/online-payments/release-notes -->
<?php
require __DIR__ . '/vendor/autoload.php';
// ----- https://docs.adyen.com/online-payments/drop-in-web?utm_source=ca_test&tab=codeBlockRDCd3_4#step-1-get-available-payment-methods ----- //
// ----- Step 1: Get available payment methods ----- //
// Set your X-API-KEY with the API key from the Customer Area.
$client = new \Adyen\Client();
$client->setEnvironment(\Adyen\Environment::TEST);
$client->setXApiKey("xxx");
$service = new \Adyen\Service\Checkout($client);
$params = array(
"merchantAccount" => "xxx",
"countryCode" => "NL",
"shopperLocale" => "nl-NL",
"amount" => array(
"currency" => "EUR",
"value" => 1000
),
"channel" => "Web"
);
$result = $service->paymentMethods($params);
// ----- Step 2: Add Drop-in to your payments form ----- //
// 2.1 Add script to bottom //
// 2.2 Add CSS to head //
// 2.3 Create a DOM element //
echo "<div id='dropin-container'></div>";
// 2.4 Create a configuration object //
?>
<script>
const configuration = {
paymentMethodsResponse: paymentMethodsResponse, // The `/paymentMethods` response from the server.
clientKey: "xxx", // Web Drop-in versions before 3.10.1 use originKey instead of clientKey.
locale: "en-US",
environment: "test",
onSubmit: (state, dropin) => {
// Your function calling your server to make the `/payments` request
makePayment(state.data)
.then(response => {
if (response.action) {
// Drop-in handles the action object from the /payments response
dropin.handleAction(response.action);
} else {
// Your function to show the final result to the shopper
showFinalResult(response);
}
})
.catch(error => {
throw Error(error);
});
},
onAdditionalDetails: (state, dropin) => {
// Your function calling your server to make a `/payments/details` request
makeDetailsCall(state.data)
.then(response => {
if (response.action) {
// Drop-in handles the action object from the /payments response
dropin.handleAction(response.action);
} else {
// Your function to show the final result to the shopper
showFinalResult(response);
}
})
.catch(error => {
throw Error(error);
});
},
paymentMethodsConfiguration: {
card: { // Example optional configuration for Cards
hasHolderName: true,
holderNameRequired: true,
enableStoreDetails: true,
hideCVC: false, // Change this to true to hide the CVC field for stored cards
name: 'Credit or debit card'
}
}
};
// 2,5 Use the configuration object to create an instance of Adyen Checkout. Then use the returned value to create and mount the instance of Drop-in: //
const checkout = new AdyenCheckout(configuration);
const dropin = checkout.create('dropin').mount('#dropin-container');
// 2.6 Pass the state.data to your server. //
{
isValid: true,
data: {
paymentMethod: {
type: "scheme",
encryptedCardNumber: "adyenjs_0_1_18$k7s65M5V0KdPxTErhBIPoMPI8HlC..",
encryptedExpiryMonth: "adyenjs_0_1_18$p2OZxW2XmwAA8C1Avxm3G9UB6e4..",
encryptedExpiryYear: "adyenjs_0_1_18$CkCOLYZsdqpxGjrALWHj3QoGHqe+..",
encryptedSecurityCode: "adyenjs_0_1_18$XUyMJyHebrra/TpSda9fha978+.."
holderName: "S. Hopper"
}
}
}
</script>
<?php
// ----- Step 3: Make a payment ----- //
// Set your X-API-KEY with the API key from the Customer Area.
$client = new \Adyen\Client();
$client->setEnvironment(\Adyen\Environment::TEST);
$client->setXApiKey("xxx");
$service = new \Adyen\Service\Checkout($client);
// STATE_DATA is the paymentMethod field of an object passed from the front end or client app, deserialized from JSON to a data structure.
$paymentMethod = STATE_DATA;
$params = array(
"merchantAccount" => "xxx",
"paymentMethod" => $paymentMethod,
"amount" => array(
"currency" => "EUR",
"value" => 1000
),
"reference" => "xxx",
"returnUrl" => "xxx"
);
$result = $service->payments($params); //causing an error
// Check if further action is needed
if (array_key_exists("action", $result)){
// Pass the action object to your front end
// $result["action"]
}
else {
// No further action needed, pass the resultCode to your front end
// $result['resultCode']
}
// ----- Step 4: Perform additional front-end actions ----- //
// 4.1 URL-decode the redirectResult appended to your return URL and pass the parameters to your back end. //
// ----- Step 5: Submit additional payment details ----- //
// Set your X-API-KEY with the API key from the Customer Area.
$client = new \Adyen\Client();
$client->setEnvironment(\Adyen\Environment::TEST);
$client->setXApiKey("xxx");
$service = new \Adyen\Service\Checkout($client);
// STATE_DATA is an object passed from the front end or client app, deserialized from JSON to a data structure.
$params = STATE_DATA;
$result = $service->paymentsDetails($params);
// Check if further action is needed
if (array_key_exists("action", $result)){
// Pass the action object to your frontend.
// $result["action"]
}
else {
// No further action needed, pass the resultCode to your front end
// $result['resultCode']
}
?>
// ----- Step 6: Present the payment result ----- //
<script>
// Show a success message
dropin.setStatus('success');
dropin.setStatus('success', { message: 'Payment successful!' });
// Show an error message
dropin.setStatus('error');
dropin.setStatus('error', { message: 'Something went wrong.'});
// Set a loading state
dropin.setStatus('loading'); // start the loading state
dropin.setStatus('ready'); // set back to the initial state
</script>
<!-- Adyen Script, Step 2 -->
<script src="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/3.21.0/adyen.js"
integrity="ha384-XpFdeUhSQQeuZeLjRcNvDBK/16avmyQiiF0t3iXT1Q/4n9b6TKM68T+hv5aZdsvc"
crossorigin="anonymous"></script>
<!-- Adyen provides the SRI hash that you include as the integrity attribute. Refer to our release notes to get the SRI hash for the specific version. https://docs.adyen.com/online-payments/release-notes -->
I've tried:
Checking dependencies,
Changing location of scripts,
Changing different key types,
Contacting Adyen.
At the moment it feels like the more time I spend on this, the further I get from solving the problem. So any help would be appreciated.

Related

Auth0 "The JWT string must contain two dots"

I'm currently using Vue3 and have integrated Auth0-spa-js from https://github.com/auth0/auth0-spa-js. This works great.
I'm sending requests to a PHP API backend through Axios, passing in the access token as a GET parameter called token.
Server side I get an exception "The JWT string must contain two dots" after setting up steps from https://github.com/auth0/auth0-PHP. I've installed the requirements, guzzle and dotenv, etc. Currently on PHP 7.4.2.
// useAuth0.js
// to login and maintain Auth state
import createAuth0Client from "#auth0/auth0-spa-js";
import { reactive } from "vue";
export const AuthState = reactive({
user: null,
loading: false,
isAuthenticated: null,
auth0: null,
});
const config = {
domain: import.meta.env.VITE_AUTH0_DOMAIN,
client_id: import.meta.env.VITE_AUTH0_CLIENT_ID,
};
export const useAuth0 = (state) => {
const handleStateChange = async () => {
state.isAuthenticated = !!(await state.auth0.isAuthenticated());
state.user = await state.auth0.getUser();
state.loading = false;
};
const initAuth = () => {
state.loading = true;
createAuth0Client({
domain: config.domain,
client_id: config.client_id,
cacheLocation: "localstorage",
redirect_uri: window.location.origin,
}).then(async (auth) => {
state.auth0 = auth;
await handleStateChange();
});
};
const login = async () => {
await state.auth0.loginWithPopup();
await handleStateChange();
};
const logout = async () => {
state.auth0.logout({
returnTo: window.location.origin,
});
};
return {
login,
logout,
initAuth,
};
};
// and I use this on a button click event
AuthState.auth0.getTokenSilently().then(accessToken => {
// AXIOS REQUEST
})
// PHP
// Auth0 SDK is 8.1.0
use Auth0\SDK\Auth0;
use Auth0\SDK\Utility\HttpResponse;
use Auth0\SDK\Token;
$env = (Dotenv\Dotenv::createImmutable(FCPATH))->load();
// I've checked that $env does contain correct .env values
$token = filter_var($_GET['token'] ?? null, FILTER_UNSAFE_RAW, FILTER_NULL_ON_FAILURE);
// Actual token I logged
eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiaXNzIjoiaHR0cHM6Ly9kZXYtd2kxeGRtbDcudXMuYXV0aDAuY29tLyJ9..V50FRJnBnpBnHJjA.e3PZuESoGaPCjp0kO9vlijGMIfhXWQHlbvsslWtbAvFAQ5hef9_PXLD_W282Cba9D6k-FAwhro9i3e5ukzXouGWYfoYHHQ5WQJ-vpLISrRANxFvNVPsCZSkg1sAIbL0Qk3Gir82ds1G919uEPc6vB3Y2qbARAd9nlMJBpLqWUq9VcIrzHtsJN7Q8j36vTCRXyu0f5-TeOr-dU3-gaIUvur37YQD0xICr4sENFktPU3s-uqCSCopVi6MoZMGvfYcVlO3nv1Sb2owGX_S_PSG7fug4Et-pMw1cVYgfNtLQf8XViI-l19sgXAf2eQShmLPvcdBdXVPA0g.S9vyktmK7rPoM_F3nUSEvg
$auth0 = new Auth0([
'domain' => $env['AUTH0_DOMAIN'],
'clientId' => $env['AUTH0_CLIENT_ID'],
'clientSecret' => $env['AUTH0_CLIENT_SECRET'],
'tokenAlgorithm' => 'RS256'
]);
// Exception thrown here with decode
$token = $auth0->decode($token, null, null, null, null, null, null, Token::TYPE_ID_TOKEN);
$token->verify();
$token->validate();
Is there and issue with auth0-spa-js when creating the token thats not compatible with Auth0 PHP SDK, or a configuration setting is not being passed that I need to add? I've pretty much configured things as those two docs specify, double checking expected variables.
Turns out I needed to add the audience parameter to the createAuth0Client, getTokenSilently(), and the PHP SDK decode method for my Auth0 Custom API. Everything validated.
I must of missed something in the docs, or it seems that the audience parameter is more of a required than optional value.
Adding to Brian Barton's answer above, the GetTokenSilentlyOptions interface explains that the options should be passed as follows:
// ✅ Like this!
const token = await this.$auth.getTokenSilently({
authorizationParams: {
audience: 'https://api.example.com/',
},
})
// ❌ NOT like this (where I got stuck for a while)
const token = await this.$auth.getTokenSilently({
audience: 'https://api.example.com/',
})
It wasn't immediately obvious to me that the additional outer object structure was required so I couldn't figure out why I couldn't get their solution to work.
Additional Context
This has NOTHING to do with what you have on the server side and everything to do with how your SPA retrieves your JWT tokens following the Authorization Code Flow with Proof Key for Code Exchange (PKCE) (which is the flow you should be using if you're accessing an API from an SPA).
I ended up finding this answer because when I failed to set the audience parameter correctly, Auth0 did NOT signal that there were any errors whatsoever. It returned a "token" that NOT a well-formed JWT. In my case it consistently had four or five dots (periods) when there should have been exactly two.

How to capture payment data from Stripe Payment Element

I've finally managed to implement the new Stripe Payment Element in Laravel via the Payment Intents API. However, I now need to capture information about the payments and store them in my database - specifically, I need the following data:
Transaction ID
Payment status (failed/pending/successful, etc.)
Payment method type (card/Google Pay/Apple Pay/etc.)
The amount actually charged to the customer
The currency the customer actually paid in
The postcode entered by the user in the payment form
All of this information seems to be available in the Payment Intent object but none of the several Stripe guides specify how to capture them on the server. I want to avoid using webhooks because they seem like overkill for grabbing and persisting data that I'm already retrieving.
It also doesn't help that, thanks to how the Stripe documentation's AJAX/PHP solution is set up, trying to dump and die any variables on the server-side causes the entire client-side flow to break, stopping the payment form from rendering and blocking any debugging information. Essentially, this makes the entire implementation of Payment Intents API impossible to debug on the server.
Does anyone who's been here before know how I would go about capturing this information?
Relevant portion of JavaScript/AJAX:
const stripe = Stripe(<TEST_PUBLISHABLE_KEY>);
const fonts = [
{
cssSrc:
"https://fonts.googleapis.com/css2?family=Open+Sans:wght#300;400;500;600;700&display=swap",
},
];
const appearance = {
theme: "stripe",
labels: "floating",
variables: {
colorText: "#2c2c2c",
fontFamily: "Open Sans, Segoe UI, sans-serif",
borderRadius: "4px",
},
};
let elements;
initialize();
checkStatus();
document
.querySelector("#payment-form")
.addEventListener("submit", handleSubmit);
// Fetches a payment intent and captures the client secret
async function initialize() {
const { clientSecret } = await fetch("/payment/stripe", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": document.querySelector('input[name="_token"]').value,
},
}).then((r) => r.json());
elements = stripe.elements({ fonts, appearance, clientSecret });
const paymentElement = elements.create("payment");
paymentElement.mount("#payment-element");
}
async function handleSubmit(e) {
e.preventDefault();
setLoading(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
// Make sure to change this to your payment completion page
return_url: "http://localhost.rc/success"
},
});
if (error.type === "card_error" || error.type === "validation_error") {
showMessage(error.message);
} else {
showMessage("An unexpected error occured.");
}
setLoading(false);
}
// Fetches the payment intent status after payment submission
async function checkStatus() {
const clientSecret = new URLSearchParams(window.location.search).get(
"payment_intent_client_secret"
);
if (!clientSecret) {
return;
}
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
switch (paymentIntent.status) {
case "succeeded":
showMessage("Payment succeeded!");
break;
case "processing":
showMessage("Your payment is processing.");
break;
case "requires_payment_method":
showMessage("Your payment was not successful, please try again.");
break;
default:
showMessage("Something went wrong.");
break;
}
}
Routes file:
Route::post('/payment/stripe', [TransactionController::class, "stripe"]);
TransactionController:
public function stripe(Request $request) {
Stripe\Stripe::setApiKey(env(<TEST_SECRET_KEY>));
header('Content-Type: application/json');
try {
$paymentIntent = Stripe\PaymentIntent::create([
'amount' => 2.99,
'currency' => 'gbp',
'automatic_payment_methods' => [
'enabled' => true,
],
]);
$output = [
'clientSecret' => $paymentIntent->client_secret,
];
$this->storeStripe($paymentIntent, $output);
echo json_encode($output);
} catch (Stripe\Exception\CardException $e) {
echo 'Error code is:' . $e->getError()->code;
$paymentIntentId = $e->getError()->payment_intent->id;
$paymentIntent = Stripe\PaymentIntent::retrieve($paymentIntentId);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
}
How can I capture the above information from the Payment Intent to store in my database?
I know you're not going to like this but I'll say it anyway. I honestly think implementing a webhook endpoint, listener and receiver function is your best bet and here is why:
The Stripe Payment Intent captures the lifecycle of a payment as it moves through multiple states. Because the various payment networks outside of Stripe do not guarantee specific response times these transitions can be asynchronous.
Therefore you cannot be certain when is the appropriate time to query the API for your completed Payment Intent unless you are listening for the payment_intent.succeeded event. Additionally, in some cases payment methods can be declined well after initial processing (e.g. suspected fraudulent cards, etc.). Using the webhooks approach keeps you informed of these changes.
Lastly, while you may only care to store this data in your DB now, scope does tend to increase and implementing webhook listeners early means you will have a solution ready if you need to take additional actions like
Sending email notifications to your customers
Adjusting revenue reconciliation
processing fulfillment actions
Other stuff.....
At the recommendation of RyanM I opted instead for the webhook solution, which turned out to be easier than I expected using Spatie's Stripe Webhooks package (although it seems to be run by someone who cares more about closing issues than fixing potential bugs, so opting for Stripe Cashier instead would probably be both easier and a more pleasant developer experience).
Note that by default Stripe webhooks return an Event object that itself contains other objects relevant to the event, such as a PaymentIntent for payment_intent.succeeded, for example, and any associated Charge objects. Therefore it's necessary to drill down a little to get all of the information needed.
$paymentIntent = $this->webhookCall->payload["data"]["object"];
$paymentID = $this->webhookCall->payload["data"]["object"]["id"]; // Transaction ID
$charge = $this->webhookCall->payload["data"]["object"]["charges"]["data"][0];
$transaction = Transaction::where("gateway_payment_id", $paymentID)->first();
$transaction->payment_status = strtoupper($paymentIntent["status"]); // Payment status
$transaction->payment_method = $charge["payment_method_details"]["type"]; // Payment method
$transaction->amount = ($paymentIntent["amount_received"]/100); // Amount charged, in pounds
$transaction->currency = strtoupper($paymentIntent["currency"]); // Currency charged in
$transaction->postcode = $charge["billing_details"]["address"]["postal_code"] ?? "N/A"; // Postcode if entered by the user - otherwise default to N/A

How to handle multiple environments: dev, staging and production with paypal webhooks?

I would like to have three environments, where I can test and use paypal webhooks. My current approach is to create new webhook for every environment in paypal developer portal and when request arrives check if it is for that environment. How does paypal handle multiple webhook urls and what status code should be returned if environment is not correct?
Create a Class like below to keep in the beginning of your application. Let it initialize once.
class App {
private static $env_status = null;
private static $paypal_settings = [];
const ENV_PRODUCTION = "production";
const ENV_STAGING = "staging";
const ENV_DEV = "development";
public static function init() {
// Set environment status.
// You can also parse domain name and depending on your domain, you can set the environment status.
self::$env_status = getenv("ENV_STATUS"); // getenv() Gets from Environment variable. You'll need set clear_env = no in php config for this to work.
switch(self::$env_status) {
case App::ENV_PRODUCTION:
self::$paypal_settings = [
"mode" => "live"
"clientID" => "PRODUCTION_CLIENT_ID" ,
"secret" => "PRODUCTION_SECRET" ,
"currency" => "USD",
"webhook" => "https://example.com/live_webhook_endpoint"
];
break;
case App::ENV_STAGING:
self::$paypal_settings = [
"mode"=> "sandbox"
"clientID"=> "STAGING_CLIENT_ID" ,
"secret"=> "STAGING_SECRET" ,
"currency"=> "USD",
"webhook" => "https://example.com/staging_webhook_endpoint"
];
break;
default:
// ENV_DEV settings
self::$paypal_settings = [
"mode"=> "sandbox"
"clientID"=> "DEVELOPMENT_CLIENT_ID" ,
"secret"=> "DEVELOPMENT_SECRET" ,
"currency"=> "USD",
"webhook" => "https://example.com/development_webhook_endpoint"
];
break;
}
}
public static function env_status() {
return self::$env_status;
}
public static function paypal_settings() {
return self::$paypal_settings;
}
// You can also create seprate function if you just want webhook URL.
// You can define in different variable also if that's the case.
public static function paypal_webhook_url() {
return self::$paypal_settings['webhook'];
}
} App::init();
Then whenever you want to get paypal settings you can call it from anywhere in your Application.
$paypay_settings = App::paypal_settings();
OR if you need just paypal webhook URL
$paypal_webhook_url = App::paypal_webhook_url();
This way you don't have to keep any conditions in other parts of your code. All the conditions will go in a single place, which will be easier to update later.
How does paypal handle multiple webhook urls.
You will need to hit PayPal Sandbox URL to hit for staging/development environment.
What status code should be returned if environment is not correct?
HTTP 400. Since it will be an invalid request.
Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400
The HyperText Transfer Protocol (HTTP) 400 Bad Request response status code indicates that the server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
On one of our projetcs, we use different configuration variables depending on the environnement.
For dev
$config['paypal.settings'] = array(
'mode'=> "sandbox", //'live' or 'sandbox'(default)
'clientID'=>"CLIENT_ID_FOR_DEV" ,
'secret'=> "SUPERSECRETCLIENT_SECRET_FOR_DEV" ,
'currency'=>'EUR',
'logEnabled' => false,
'logDir'=>__DIR__ . '/../logs'
);
For prod :
$config['paypal.settings'] = array(
'mode'=> "live",
'clientID'=>"CLIENT_ID_FOR_PROD" ,
'secret'=> "SUPERSECRETCLIENT_SECRET_FOR_PROD" ,
'currency'=>'EUR',
'logEnabled' => false,
'logDir'=>__DIR__ . '/../logs'
);
and our dev and prod environnement are obviously on 2 different domains that are configured for each CLIENT_ID on the paypal interface.
In the webhook controller called by Paypal we have :
class WebhookController{
function paypalPingBackAction($request){
$paypalSettings = //get paypal settings
$isLive = ($paypalSettings["sandbox"] ?? "sandbox") == "live";
$currentDomain = $request->getDomain();
// now we have enough information (domain and sandbox)
// to do things accordingly
}
}
Paypal does not really care about what code you send back. But if you send a 500, he will retry later. So maybe, if things go well, just return an empty 201 accepted !

Notify frontend Vue SPA if Stripe (iDEAL) charge is succeeded with Webhooks and Jobs Laravel

So I'm building an webapp that has a shop with Laravel API and Vue as the frontend SPA.
I've been trying to use Strip to enable payments. So far, with the help of Stripe's documentation, I have been able to create a Source in the frontend. for iDEAL, Stripe highly suggests us to make use of webhooks to confirm whether a payment has succeeded. (I'm using Spatie/Laravel-Stripe-Webhook package) This is the current flow of my webapp:
Checkout.vue:
checkout() {
const sourceData = {
type: 'ideal',
amount: this.cart.total,
currency: 'eur',
owner: {
name: this.name + ' ' + this.last_name,
email: this.email,
},
metadata: {
order: JSON.stringify(order),
total_quantity: this.cart.total_quantity,
},
redirect: {
return_url: 'http://example.test/order-confirmation',
},
}
this.stripe.createSource(this.ideal, sourceData).then(function(result) {
if (result.error) {
console.log(error.message)
this.error = error.message
} else {
stripeSourceHandler(result.source)
}
})
const stripeSourceHandler = source => {
document.location.href = source.redirect.url
}
},
After filling in billing address, emails etc. the user starts the payment.
User gets redirected to iDEAL payment page where they can authorize payment.
The Source is now created. Stripe sends source.chargeable webhook:
config/stripe-webhooks.php:
'jobs' => [
'source_chargeable' => \App\Jobs\StripeWebhooks\ProcessPaymentsJob::class,
'charge_succeeded' => \App\Jobs\StripeWebhooks\ChargeSucceededJob::class,
],
ProcessPaymentsJob.php:
public function __construct(WebhookCall $webhookCall)
{
$this->webhookCall = $webhookCall;
}
public function handle()
{
$charge = $this->webhookCall->payload['data']['object'];
\Stripe\Stripe::setApiKey(config('services.stripe.secret'));
$user = '';
if(User::find(Auth::id())) {
$user = $user->name;
} else {
$user = 'a guest';
}
$payment = \Stripe\Charge::create([
'amount' => $charge['amount'],
'currency' => 'eur',
'source' => $charge['id'],
'description' => 'New payment from '. $user,
'metadata' => [
'order' => $charge['metadata']['order'],
'total_quantity' => $charge['metadata']['total_quantity'],
]
]);
}
User returns to redirect[return_url]
If all went well, Stripe should send charge.succeeded webhook:
ChargeSucceededJob.php:
public function __construct(WebhookCall $webhookCall)
{
$this->webhookCall = $webhookCall;
}
public function handle()
{
$charge = $this->webhookCall->payload['data']['object'];
$order = Order::create([
'user_id' => Auth::id() ?? null,
'payment_id' => $charge['id'],
'payment_method' => $charge['payment_method_details']['type'],
'billing_email' => $charge['billing_details']['email'],
'billing_name' => $charge['metadata']['name'],
'billing_last_name' => $charge['metadata']['last_name'],
'billing_address' => $charge['metadata']['address'],
'billing_address_number' => $charge['metadata']['address_num'],
'billing_postal_code' => $charge['metadata']['postal_code'],
'billing_city' => $charge['metadata']['city'],
'billing_phone' => strval($charge['billing_details']['phone']),
'order' => json_decode($charge['metadata']['order']),
'total_quantity' => (int) $charge['metadata']['total_quantity'],
'billing_total' => $charge['amount'],
]);
}
This is all going well. However, I do not know how to notify the customer (on the frontend) that the order has been completed. In Stripe's documentation, they explain how to retrieve the Source on the order confirmation page, but they do not explain how to retrieve the Charge, because this is what determines whether the whole order has been completed or not.
OrderConfirmation.vue:
checkPaymentStatus() {
this.stripe = Stripe(this.stripeKey)
// After some amount of time, we should stop trying to resolve the order synchronously:
const MAX_POLL_COUNT = 10;
let pollCount = 0;
let params = new URLSearchParams(location.search)
const pollForSourceStatus = async () => {
const { source } = await this.stripe.retrieveSource({id: params.get('source'), client_secret: params.get('client_secret')})
if (source.status === 'chargeable') {
// Make a request to your server to charge the Source.
// Depending on the Charge status, show your customer the relevant message.
} else if (source.status === 'pending' && pollCount < MAX_POLL_COUNT) {
// Try again in a second, if the Source is still `pending`:
pollCount += 1;
setTimeout(pollForSourceStatus, 1000);
} else {
// Depending on the Source status, show your customer the relevant message.
}
};
pollForSourceStatus();
}
How do I go from here? I am trying to notify the frontend when the Charge has been succeeded. My initial thought process was just to return the Order object, as I would do if it was a Controller, but if I understand correctly, the Job is running asynchronously, so I can't return data. I am also new to Jobs and Queues and stuff, I'm still trying to wrap my head around with it.
Another option I thought of is that I would poll requests from the frontend to the backend to request the last Order, but I have no idea how this would work and/or if this is a good solution.
Any help/tips/helpful resources would be much appreciated!
iDEAL payments are asynchronous, but they luckily do immediately notify you if the payment was successful or not.
When the iDEAL process is complete and your user is redirected to your site, Stripe automatically appends some query parameters to the URL. Meaning your users will be redirected to something like:
https://example.com/checkout/complete?payment_intent=pi_123&payment_intent_client_secret=pi_123_secret_456&source_type=ideal
The next step is to then retrieve the PaymentIntent and check on its status, which you can do by either:
Retrieving the PaymentIntent on the client using stripe.js and the PaymentIntent client secret: https://stripe.com/docs/js/payment_intents/retrieve_payment_intent
Retrieving the PaymentIntent on the server by sending an ajax request to your backend with the PaymentIntend ID: https://stripe.com/docs/api/payment_intents/retrieve
If the status is succeeded, then the payment was completed and you can proceed from there.

Response object - Payment with Mollie and Omnipay

I'm trying to create a payment with Omnipay and Mollie in my Laravel project. I'm using the following 2 libraries:
https://github.com/barryvdh/laravel-omnipay
https://github.com/thephpleague/omnipay-mollie
I'm doing the following in my code:
$gateway = Omnipay\Omnipay::create('Mollie');
$gateway->setApiKey('test_gSDS4xNA96AfNmmdwB3fAA47zS84KN');
$params = [
'amount' => $ticket_order['order_total'] + $ticket_order['organiser_booking_fee'],
'description' => 'Bestelling voor klant: ' . $request->get('order_email'),
'returnUrl' => URL::action('EventCheckoutController#fallback'),
];
$response = $gateway->purchase($params)->send();
if ($response->isSuccessful()) {
session()->push('ticket_order_' . $event_id . '.transaction_id',
$response->getTransactionReference());
return $this->completeOrder($event_id);
}
The payment works. When the payment is done he goes back to the function fallback. But I don't know what to put in this function and how to go back to the line if($response->isSuccesfull()...).
The most important thing I need to do after the payment is :
session()->push('ticket_order_' . $event_id . '.transaction_id',
$response->getTransactionReference());
return $this->completeOrder($event_id);
Can someone help me figure out how to work with the fallback function and above?
A typical setup using Mollie consists of three separate pages:
a page to create the payment;
a page where Mollie posts the final payment state to in the background; and
a page where the consumer returns to after the payment.
The full flow is described in the Mollie docs. Also take a look at the flow diagram at that page.
DISCLAIMER: I've never used Omnipay myself and did not test the following code, but it should at least give you an idea how to set up your project.
Creating the payment:
$gateway = Omnipay\Omnipay::create('Mollie');
$gateway->setApiKey('test_gSDS4xNA96AfNmmdwB3fAA47zS84KN');
$params = [
'amount' => $ticket_order['order_total'] + $ticket_order['organiser_booking_fee'],
'description' => 'Bestelling voor klant: ' . $request->get('order_email'),
'notifyUrl' => '', // URL to the second script
'returnUrl' => '', // URL to the third script
];
$response = $gateway->purchase($params)->send();
if ($response->isRedirect()) {
// Store the Mollie transaction ID in your local database
store_in_database($response->getTransactionReference());
// Redirect to the Mollie payment screen
$response->redirect();
} else {
// Payment failed: display message to the customer
echo $response->getMessage();
}
Receiving the webhook:
$gateway = Omnipay\Omnipay::create('Mollie');
$gateway->setApiKey('test_gSDS4xNA96AfNmmdwB3fAA47zS84KN');
$params = [
'transactionReference' => $_POST['id'],
];
$response = $gateway->fetchTransaction($params);
if ($response->isPaid()) {
// Store in your local database that the transaction was paid successfully
} elseif ($response->isCancelled() || $response->isExpired()) {
// Store in your local database that the transaction has failed
}
Page where the consumer returns to:
// Check the payment status of your order in your database. If the payment was paid
// successfully, you can display an 'OK' message. If the payment has failed, you
// can show a 'try again' screen.
// Most of the time the webhook will be called before the consumer is returned. For
// some payment methods however the payment state is not known immediately. In
// these cases you can just show a 'payment is pending' screen.

Categories