Skip to content

Commit

Permalink
Adjust for Paddle Billing v2 (#100)
Browse files Browse the repository at this point in the history
* Adjust for Paddle Billing v2

* Fix failing transactions

* Paddle updates

* Rebase

* Update cancellation webhook

* Update cancel button

* Fix the cancel and update buttons

* Update the docs

* Fix plan switching

* Adding updates to the checkout

* Adding functionality so that way the invoice downloads work again

* Adding updates to apikey

* Fix a typo

* Update docs api key references

* Get the latest subscription

* Don't list subscriptions if none

---------

Co-authored-by: Tony Lea <tnylea@gmail.com>
  • Loading branch information
bobbyiliev and tnylea committed Apr 1, 2024
1 parent 3ab5af8 commit cfe8ff3
Show file tree
Hide file tree
Showing 19 changed files with 14,689 additions and 237 deletions.
2 changes: 1 addition & 1 deletion .env.example
Expand Up @@ -51,7 +51,7 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
JWT_SECRET=Jrsweag3Mf0srOqDizRkhjWm5CEFcrBy

PADDLE_VENDOR_ID=
PADDLE_VENDOR_AUTH_CODE=
PADDLE_API_KEY=
PADDLE_ENV=sandbox
PADDLE_PUBLIC_KEY=

Expand Down
4 changes: 3 additions & 1 deletion config/wave.php
Expand Up @@ -22,7 +22,9 @@

'paddle' => [
'vendor' => env('PADDLE_VENDOR_ID', ''),
'auth_code' => env('PADDLE_VENDOR_AUTH_CODE', ''),
'auth_code' => env('PADDLE_API_KEY', ''),
'api_key' => env('PADDLE_API_KEY', ''),
'client_side_token' => env('PADDLE_CLIENT_SIDE_TOKEN', ''),
'env' => env('PADDLE_ENV', 'sandbox'),
'public_key' => env('PADDLE_PUBLIC_KEY', ''),
]
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4,270 changes: 4,269 additions & 1 deletion public/themes/tailwind/css/app.css

Large diffs are not rendered by default.

9,958 changes: 9,956 additions & 2 deletions public/themes/tailwind/js/app.js

Large diffs are not rendered by default.

46 changes: 0 additions & 46 deletions resources/views/themes/tailwind/assets/js/app.js
Expand Up @@ -188,52 +188,6 @@ window.popToast = function(type, message){

/********** END TOAST FUNCTIONALITY **********/

/********** Start Billing Checkout Functionality ***********/

/***** Payment Success Functionality */

window.checkoutComplete = function(data) {
var checkoutId = data.checkout.id;

Paddle.Order.details(checkoutId, function(data) {
// Order data, downloads, receipts etc... available within 'data' variable.
document.getElementById('fullscreenLoaderMessage').innerText = 'Finishing Up Your Order';
document.getElementById('fullscreenLoader').classList.remove('hidden');
axios.post('/checkout', { _token: csrf, checkout_id: data.checkout.checkout_id })
.then(function (response) {
console.log(response);
if(parseInt(response.data.status) == 1){
let queryParams = '';
if(parseInt(response.data.guest) == 1){
queryParams = '?complete=true';
}
window.location = '/checkout/welcome' + queryParams;
}
});
});
}

window.checkoutUpdate = function(data){
if(data.checkout.completed){
popToast('success', 'Your payment info has been successfully updated.');
} else {
popToast('danger', 'Sorry, there seems to be a problem updating your payment info');
}
}

window.checkoutCancel = function(data){
let subscriptionId = data.checkout.id;
axios.post('/cancel', { _token: csrf, id: subscriptionId })
.then(function (response) {
if(parseInt(response.data.status) == 1){
window.location = '/settings/subscription';
}
});
}

/***** End Payment Success Functionality */

/********** End Billing Checkout Functionality ***********/

/********** Switch Plans Button Click ***********/

Expand Down
27 changes: 22 additions & 5 deletions resources/views/themes/tailwind/partials/cancel-modal.blade.php
Expand Up @@ -29,10 +29,9 @@
</div>
<div class="mt-5 sm:mt-6 sm:flex sm:flex-row-reverse">
<span class="flex flex-1 w-full rounded-md shadow-sm sm:ml-3 sm:w-full">
<div data-url="{{ auth()->user()->subscription->cancel_url }}" @click="$store.confirmCancel.open=false" class="inline-flex justify-center w-full px-4 py-2 text-base font-medium leading-6 text-white transition duration-150 ease-in-out bg-red-600 border border-transparent rounded-md shadow-sm cursor-pointer checkout-cancel hover:bg-red-500 focus:outline-none focus:border-red-700 focus:shadow-outline-red sm:text-sm sm:leading-5">
Cancel Subscription
</div>

<div data-url="{{ auth()->user()->subscription->cancel_url }}" id="cancelSubscriptionButton" class="inline-flex justify-center w-full px-4 py-2 text-base font-medium leading-6 text-white transition duration-150 ease-in-out bg-red-600 border border-transparent rounded-md shadow-sm cursor-pointer checkout-cancel hover:bg-red-500 focus:outline-none focus:border-red-700 focus:shadow-outline-red sm:text-sm sm:leading-5">
Cancel Subscription
</div>
</span>
<span class="flex flex-1 w-full mt-3 rounded-md shadow-sm sm:mt-0 sm:w-full">
<button onclick="closeCancelModal()" type="button" class="inline-flex justify-center w-full px-4 py-2 text-base font-medium leading-6 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md shadow-sm hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue sm:text-sm sm:leading-5">
Expand All @@ -50,4 +49,22 @@
window.closeCancelModal = function(){
Alpine.store('confirmCancel').close();
}
</script>
document.getElementById('cancelSubscriptionButton').addEventListener('click', function() {
fetch('/cancel', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content'),
'Content-Type': 'application/json'
},
body: JSON.stringify({ subscription_id: this.getAttribute('data-url') })
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
closeCancelModal();
})
.catch(error => {
console.error('Error:', error);
});
});
</script>
Expand Up @@ -3,12 +3,12 @@
@subscriber
@php
$subscription = new \Wave\Http\Controllers\SubscriptionController;
$invoices = $subscription->invoices( auth()->user() );
$transactions = $subscription->transactions( auth()->user() );
@endphp



@if(isset($invoices->success) && $invoices->success == true)
@if(count($transactions) > 0)

<table class="min-w-full overflow-hidden divide-y divide-gray-200 rounded-lg">
<thead>
Expand All @@ -25,17 +25,17 @@
</tr>
</thead>
<tbody>
@foreach($invoices->response as $invoice)
@foreach($transactions as $transaction)
<tr class="@if($loop->index%2 == 0){{ 'bg-gray-50' }}@else{{ 'bg-gray-100' }}@endif">
<td class="px-6 py-4 text-sm font-medium leading-5 text-gray-900 whitespace-no-wrap">
{{ Carbon\Carbon::parse($invoice->payout_date)->toFormattedDateString() }}
{{ Carbon\Carbon::parse($transaction->created_at)->toFormattedDateString() }}
</td>
<td class="px-6 py-4 text-sm font-medium leading-5 text-right text-gray-900 whitespace-no-wrap">
${{ $invoice->amount }}
{{ $transaction->details->totals->subtotal }}
</td>
<td class="px-6 py-4 text-sm font-medium leading-5 text-right whitespace-no-wrap">
<a href="{{ $invoice->receipt_url }}" target="_blank" class="mr-2 text-indigo-600 hover:underline focus:outline-none">
Download
<a href="/settings/invoices/{{ $transaction->id }}" target="_blank" class="mr-2 text-indigo-600 hover:underline focus:outline-none">
Generate Invoice
</a>
</td>

Expand All @@ -45,7 +45,7 @@
</table>

@else
<p>Sorry, there seems to be an issue retrieving your invoices or you may not have any invoices yet.</p>
<p>You currently do not have any invoices associated with your account</p>
@endif

@notsubscriber
Expand Down
Expand Up @@ -6,7 +6,7 @@
<div class="flex flex-col">
<h5 class="mb-2 text-xl font-bold text-gray-700">Modify Payment Information</h5>
<p>Click the button below to update your default payment method</p>
<button data-url="{{ auth()->user()->subscription->update_url }}" class="inline-flex self-start justify-center w-auto px-4 py-2 mt-5 text-sm font-medium text-white transition duration-150 ease-in-out border border-transparent rounded-md checkout-update bg-wave-600 hover:bg-wave-500 focus:outline-none focus:border-wave-700 focus:shadow-outline-wave active:bg-wave-700">Update Payment Info</button>
<a href="{{ auth()->user()->subscription->update_url }}" class="inline-flex self-start justify-center w-auto px-4 py-2 mt-5 text-sm font-medium text-white transition duration-150 ease-in-out border border-transparent rounded-md checkout-update bg-wave-600 hover:bg-wave-500 focus:outline-none focus:border-wave-700 focus:shadow-outline-wave active:bg-wave-700">Update Payment Info</a>
</div>

<hr class="my-8 border-gray-200">
Expand All @@ -29,4 +29,4 @@
window.cancelClicked = function(){
Alpine.store('confirmCancel').openModal();
}
</script>
</script>
@@ -0,0 +1,42 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class ChangeSubscriptionIdToString extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('paddle_subscriptions', function (Blueprint $table) {
// Change subscription_id and plan_id columns to string type
$table->string('subscription_id', 255)->change();
$table->string('plan_id', 255)->change();
// Adjusting the length of the cancel_url and update_url columns to 500 (or a value that suits your needs)
$table->text('cancel_url')->change();
$table->text('update_url')->change();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('paddle_subscriptions', function (Blueprint $table) {
// Revert back to integer type if needed
$table->integer('subscription_id')->change();
$table->integer('plan_id')->change();
// Reverting the length back to 255
$table->string('cancel_url', 255)->change();
$table->string('update_url', 255)->change();
});
}
}
3 changes: 2 additions & 1 deletion wave/docs/configurations.md
Expand Up @@ -77,7 +77,8 @@ return [

'paddle' => [
'vendor' => env('PADDLE_VENDOR_ID', ''),
'auth_code' => env('PADDLE_VENDOR_AUTH_CODE', ''),
'auth_code' => env('PADDLE_API_KEY', ''),
'client_side_token' => env('PADDLE_CLIENT_SIDE_TOKEN', ''),
'env' => env('PADDLE_ENV', 'sandbox')
]

Expand Down
29 changes: 25 additions & 4 deletions wave/docs/features/billing.md
Expand Up @@ -15,7 +15,7 @@ In order to integrate your application with Paddle you will need to signup for a

After you have created your Paddle Account you'll be able to login and see your dashboard, which should look similar to the following:

![paddle-dashboard.png](https://cdn.devdojo.com/images/april2021/paddle-dashboard.png)
![paddle-dashboard.png](https://imgur.com/SyNZ0W9.png)

Next, let's add your Paddle API credentials.

Expand All @@ -24,18 +24,39 @@ Next, let's add your Paddle API credentials.

Inside of your Paddle Dashboard you'll see a button under the **Developer Tools** menu, called **Authentication**, click on that button to get your API Authentication Credentials.

![paddle-authentication.png](https://cdn.devdojo.com/images/april2021/paddle-authentication.png)
![paddle-authentication.png](https://imgur.com/xdDuVKn.png)

On this page you'll find your **Vendor ID** and your **API Auth Code**. These are the credentials that you will need to add to your `.env` file for `PADDLE_VENDOR_ID` and `PADDLE_VENDOR_AUTH_CODE`:
Along with the **API Auth Code**, you'll also need to get your **Client Side Token**.

On this page you'll find your **Seller ID** and your **API Auth Code**. These are the credentials that you will need to add to your `.env` file for `PADDLE_VENDOR_ID`, `PADDLE_API_KEY` and `PADDLE_CLIENT_SIDE_TOKEN`:

```
PADDLE_VENDOR_ID=9999
PADDLE_VENDOR_AUTH_CODE=YOUR_REALLY_LONG_API_KEY_HERE
PADDLE_API_KEY=YOUR_REALLY_API_KEY_HERE
PADDLE_CLIENT_SIDE_TOKEN=YOUR_CLIENT_SIDE_TOKEN
PADDLE_ENV=sandbox
```

After adding these credentials, your application has been successfully configured with Paddle.

## Default payment link

Wave uses the default Paddle payment link to handle the payment process. You have to set up the default payment link in your Paddle account. To do this, go to your Paddle dashboard and click on **Checkout Settings** scroll down to **Payment Links**.

The default payment link should be set to `http://yourdomain.com/settings/subscription`.

![](https://imgur.com/zboWobt.png)

## Webhooks

Wave uses Paddle webhooks to handle the payment process. You have to set up the webhooks in your Paddle account. To do this, go to your Paddle dashboard and click on **Developer Tools** -> **Notifications**.

![](https://imgur.com/QqJTggu.png)

Make sure to select the `subscription.cancelled` event so that Wave can handle the subscription cancellation process in case a user cancels their subscription or their payment fails.

> **Note**: Wave currently only supports the `subscription.cancelled` event. More events will be supported in the future.
#### Ready to go Live?

When you are ready to go live and take live payments you'll want to change the `PADDLE_ENV` from `sandbox` to `live`, and you'll be ready to accept live payments 💵
Expand Down
29 changes: 21 additions & 8 deletions wave/docs/features/subscription-plans.md
Expand Up @@ -60,20 +60,33 @@ Fill out the rest of the info on the plan and click `Save` to create your new pl
<a name="create-plans-paddle"></a>
### Creating Plans in Paddle

To create a new plan in Paddle, login to your dashboard and click **Catalog**->**Subscription Plans**. Click on the **+ New Plan** button at the top right to create a new plan.
To create a new plan in Paddle, login to your dashboard and click **Catalog**->**Products**. Click on the **New Product** button at the top right to create a new plan.

![paddle-plans-01.png](https://cdn.devdojo.com/images/april2021/paddle-plans-01.png)
![paddle-plans-01.png](https://imgur.com/PL8mO1n.png)

You'll see a pop-up that will ask for the plan name, icon, and price. Fill out the info for your plan.
You'll see a pop-up that will ask for the plan name and a description. Fill out the info for your plan.

![paddle-plans-02.png](https://cdn.devdojo.com/images/april2021/paddle-plans-02.png)
![paddle-plans-02.png](https://imgur.com/J4fKEYe.png)

Scroll down to the bottom and click the **Save Plan** button.
![paddle-plans-03.png](https://cdn.devdojo.com/images/april2021/paddle-plans-03.png)
Click the **Save Plan** button on the top right to save your new plan.

After creating your new plan, you'll see the **Plan ID** you need to associate with the Wave Plan you create from the previous step.
After creating your new plan, you'll need to create a new subscription as part of the plan. Click on the **New Price** button to create a new subscription.

![paddle-plans-04.png](https://cdn.devdojo.com/images/april2021/paddle-plans-04.png)
![](https://imgur.com/f2ropW0.png)

Fill out the info for your subscription, make sure to select **Recurring** for the billing type and click the **Save** button on the top right to save your new subscription.

![](https://imgur.com/SQPN1YB.png)

Next, get the subscription ID for your new subscription. It starts with `pri_` and is located under the **Subscription ID** column.

![](https://imgur.com/fRxW2yF.png)

Make sure to copy this ID as you will need it to associate the plan with the subscription in Wave.

Note that you should not use the `Product ID` for the plan, but the `Subscription ID` for the subscription.

Next, go back to your Wave dashboard and click on the plan you created. You will see a field called **Plan ID**. Paste the subscription ID you copied from Paddle into this field and click **Save**.

After adding all your plans, we're ready to [test out the billing process](/docs/features/billing#test-billing).

Expand Down

0 comments on commit cfe8ff3

Please sign in to comment.