Recently, I started trying to integrate PayPal subsriptions into a project of mine, however, it turns out I’ve started by using the old PayPal PHP SDK which is not the recommended way anymore. The recommended way is using the new Checkout PHP SDK, however that one doesn’t support subscriptions so the way to integrate PayPal subscriptions is by making direct https calls to their API. In this post I’ll show you how I started integrating the subscriptions using the old, deprecated SDK in case somebody needs it. Note that it’s not full integration (I’ve only gotten to a certain point) and it’s also Laravel specific.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 |
<?php namespace App\Logic\Paypal; use Exception; use App\Setting; use PayPal\Api\Currency; use PayPal\Api\MerchantPreferences; use PayPal\Api\PaymentDefinition; use PayPal\Api\Plan; use PayPal\Api\Patch; use PayPal\Api\PatchRequest; use PayPal\Api\VerifyWebhookSignature; use PayPal\Api\Webhook; use PayPal\Auth\OAuthTokenCredential; use PayPal\Common\PayPalModel; use PayPal\Api\Agreement; use PayPal\Api\Payer; use PayPal\Rest\ApiContext; class Paypal { public $apiContext; public function __construct() { $this->apiContext = new ApiContext( new OAuthTokenCredential( config('services.paypal.client_id'), // ClientID config('services.paypal.secret') ) ); } public function createPlan() { $monthlyAmount = 10; $currency = 'EUR'; // Create a new instance of Plan object $plan = new Plan(); // # Basic Information // Fill up the basic information that is required for the plan $plan->setName('Epic Plan') ->setDescription('Unlimited form submissions. €10 paid monthly.') ->setType('infinite'); // # Payment definitions for this billing plan. $paymentDefinition = new PaymentDefinition(); $paymentDefinition->setName('Regular Payments') ->setType('REGULAR') ->setFrequency('Day') ->setFrequencyInterval("1") ->setCycles("0") ->setAmount(new Currency(array('value' => $monthlyAmount, 'currency' => $currency))); $merchantPreferences = new MerchantPreferences(); $baseUrl = env('APP_URL'); $merchantPreferences->setReturnUrl("$baseUrl/paypal/execute-agreement?success=true") ->setCancelUrl("$baseUrl/paypal/execute-agreement?success=false") ->setAutoBillAmount("yes") ->setInitialFailAmountAction("CONTINUE") ->setMaxFailAttempts("0") ->setSetupFee(new Currency(array('value' => $monthlyAmount, 'currency' => $currency))); $plan->setPaymentDefinitions(array($paymentDefinition)); $plan->setMerchantPreferences($merchantPreferences); // ### Create Plan $output = $plan->create($this->apiContext); return $output; } public function activatePlan() { $createdPlan = $this->createPlan(); $patch = new Patch(); $value = new PayPalModel('{ "state":"ACTIVE" }'); $patch->setOp('replace') ->setPath('/') ->setValue($value); $patchRequest = new PatchRequest(); $patchRequest->addPatch($patch); $createdPlan->update($patchRequest, $this->apiContext); $plan = Plan::get($createdPlan->getId(), $this->apiContext); // try to find previous setting with plan id $setting = Setting::where('name', '=', 'active_plan_id')->first(); // save the plan id in the database if ($setting) { $setting->value = $plan->getId(); $setting->save(); } else { $newSetting = new Setting( [ 'name' => 'active_plan_id', 'value' => $plan->getId(), ] ); $newSetting->save(); } return $plan; } public function createBillingAgreement() { $setting = Setting::where('name', '=', 'active_plan_id')->first(); if (! $setting) { throw new Exception('Active plan id not found.'); } $activePlanId = $setting->value; $agreement = new Agreement(); $dateAgreementStarts = gmdate("Y-m-d\TH:i:s\Z", time() + 120); $agreement->setName('Base Agreement') ->setDescription('€10 a month in return for accessing our unlimited plan.') ->setStartDate($dateAgreementStarts); // Add Plan ID // Please note that the plan Id should be only set in this case. $plan = new Plan(); $plan->setId($activePlanId); $agreement->setPlan($plan); // Add Payer $payer = new Payer(); $payer->setPaymentMethod('paypal'); $agreement->setPayer($payer); // ### Create Agreement // Please note that as the agreement has not yet activated, we wont be receiving the ID just yet. $agreement = $agreement->create($this->apiContext); // ### Get redirect url $approvalUrl = $agreement->getApprovalLink(); return redirect($approvalUrl); } /** * Get list of all webhooks * * @return \PayPal\Api\WebhookList */ public function listAllWebhooks() { $output = Webhook::getAllWithParams([], $this->apiContext); return $output; } /** * Delete all webhooks * * @return bool */ public function deleteAllWebhooks() { $webhookList = $this->listAllWebhooks(); foreach ($webhookList->getWebhooks() as $webhook) { $webhook->delete($this->apiContext); } return true; } /** * Register primary webhooks * * @return Webhook|string */ public function registerPrimaryWebhooks() { $setting = Setting::where('name', '=', 'primary_webhooks_registered')->first(); if ($setting) { return 'primary webhooks already registered'; } $baseUrl = env('APP_URL'); $webhook = new Webhook(); $webhook->setUrl("$baseUrl/paypal/webhooks/primary"); // # Event Types // Event types correspond to what kind of notifications you want to receive on the given URL. $webhookEventTypes = array(); $webhookEventTypes[] = new \PayPal\Api\WebhookEventType( '{ "name":"PAYMENT.AUTHORIZATION.CREATED" }' ); $webhookEventTypes[] = new \PayPal\Api\WebhookEventType( '{ "name":"PAYMENT.AUTHORIZATION.VOIDED" }' ); $webhook->setEventTypes($webhookEventTypes); // ### Create Webhook $output = $webhook->create($this->apiContext); $newSetting = new Setting( [ 'name' => 'primary_webhooks_registered', 'value' => $output->getId(), ] ); $newSetting->save(); return $output; } public function validatePrimaryWebhooks() { $setting = Setting::where('name', '=', 'primary_webhooks_registered')->first(); if (! $setting) { throw new Exception('Primary webhooks not registered.'); } $webhookId = $setting->value; $requestBody = file_get_contents('php://input'); $headers = getallheaders(); $headers = array_change_key_case($headers, CASE_UPPER); $signatureVerification = new VerifyWebhookSignature(); $signatureVerification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO']); $signatureVerification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID']); $signatureVerification->setCertUrl($headers['PAYPAL-CERT-URL']); $signatureVerification->setWebhookId($webhookId); $signatureVerification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG']); $signatureVerification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME']); $signatureVerification->setRequestBody($requestBody); $output = $signatureVerification->post($this->apiContext); $status = $output->getVerificationStatus(); (new Setting( [ 'name' => 'webhook ' . date('Y-m-d H:i:s'), 'value' => $status . $requestBody, ] ))->save(); return response()->json([ 'status' => 'OK' ]); } } |
Here is the code. I’ll explain the four main methods in the class. First, ‘activatePlan’ – this will create the billing plan and activate it. Second, ‘registerPrimaryWebhooks’ – which will register some webhooks with PayPal. Third, ‘validatePrimaryWebhooks’ – which will validate/handle the primary webhooks, when one is received. And last, ‘createBillingAgreement’ – which will create a billing agreement and return a redirect to the PayPal’s approval link for that agreement.
Here is also my ‘PaypalController’ which handles the requests and also has some PayPal logic to execute the billing agreement once the user is redirected to the success url after approval.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
<?php namespace App\Http\Controllers; use App\Setting; use App\Logic\Paypal\Paypal; use Illuminate\Http\Request; use PayPal\Api\Agreement; class PaypalController extends Controller { public function createAndActivatePlan() { $setting = Setting::where('name', '=', 'active_plan_id')->first(); if ($setting) { return 'plan already activated'; } $paypal = new Paypal(); $plan = $paypal->activatePlan(); if ($plan) { return 'success'; } else { return 'failure'; } } public function redirectToPaypal() { $paypal = new Paypal(); return $paypal->createBillingAgreement(); } public function executeAgreement() { // ## Approval Status // Determine if the user accepted or denied the request if (isset($_GET['success']) && $_GET['success'] == 'true') { $paypal = new Paypal(); $token = $_GET['token']; $agreement = new Agreement(); // ## Execute Agreement // Execute the agreement by passing in the token $agreement->execute($token, $paypal->apiContext); // ## Get Agreement // Make a get call to retrieve the executed agreement details $agreement = Agreement::get($agreement->getId(), $paypal->apiContext); dd($agreement); } } public function registerPrimaryWebhooks() { $paypal = new Paypal(); return $paypal->registerPrimaryWebhooks(); } public function handlePrimaryWebhooks() { $paypal = new Paypal(); return $paypal->validatePrimaryWebhooks(); } } |
Here is also my PayPal routes in case you need it
1 2 3 4 5 6 7 8 |
<?php // PayPal |