-
Notifications
You must be signed in to change notification settings - Fork 20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Version 3: Nova Stripe Package Complete Rewrite #70
base: main
Are you sure you want to change the base?
Conversation
aa526a1
to
9ea1477
Compare
9ea1477
to
cf12898
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested it locally on a nova 4 app, and other than the Product Resources page having some minor issue with the presentation of the product fields, everything else (Subs, Charges and Customers) worked excellent!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really like the setting up a cron that dispatches this syncing process workflow
protected function stripeLink(): Attribute | ||
{ | ||
return Attribute::make( | ||
get: fn ($value, array $attributes): string => 'https://dashboard.stripe.com/' . $this->service . '/' . $attributes['id'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we rely a bunch on linking back to the stripe dashboard and the stripe links for tests, what do you think of adding a conditional prefix of something like
$isTest ? 'https://dashboard.stripe.com/test/' : 'https://dashboard.stripe.com/'
Perhaps by adding a private function like
public function isTestMode(): bool
{
return ! str($this->getService()->config->apiKey)->startsWith('sk_test');
}
While digging through the Stripe SDK wasn't able to find a nice method like Stripe::livemode()
other than by retriveing the account Stripe\Account::retrieve()->livemode
but it seemed like potentially yet another value to ensure we kept in sync.
$address = json_decode((string) $attributes['address'], true); | ||
|
||
return trim( | ||
$address['line1'] . ' ' . | ||
($address['line2'] ? $address['line2'] . ' ' : '') . | ||
$address['city'] . ', ' . | ||
$address['country'] . ' ' . | ||
$address['postal_code'] | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps the trim on each line is a bit excessive, but ran into an offset issue here, what do you think of this change?
$address = json_decode((string) $attributes['address'], true); | |
return trim( | |
$address['line1'] . ' ' . | |
($address['line2'] ? $address['line2'] . ' ' : '') . | |
$address['city'] . ', ' . | |
$address['country'] . ' ' . | |
$address['postal_code'] | |
); | |
$addressData = json_decode((string) $attributes['address'], true) ?: []; | |
$line1 = trim($addressData['line1'] ?? ''); | |
$line2 = trim($addressData['line2'] ?? ''); | |
$city = trim($addressData['city'] ?? ''); | |
$country = trim($addressData['country'] ?? ''); | |
$postalCode = trim($addressData['postal_code'] ?? ''); | |
$streetAddress = implode(' ', array_filter([$line1, $line2])); | |
$locality = implode(' ', array_filter([$city, $country, $postalCode])); | |
$formattedAddress = implode(', ', array_filter([$streetAddress, $locality])); | |
return trim($formattedAddress); |
This PR is a complete rewrite of the package. Instead of pulling data from Stripe on the fly, the new version introduces a sync process that allows the user to select resources to sync: Products, Charges, Customers, and Subscriptions. The sync process uses the Stripe SDK to import all records, enabling us to use classic Nova resources and leverage all of Nova's built-in features to address the current issues and enhancement requests.
New Features in Version 3
Welcome Dialog: When the user visits the tool's sections for the first time, a welcome dialog explaining how to use the "Sync With Stripe" action appears. Once closed, a key is saved in localStorage to prevent it from being displayed again.
Sync with Stripe: Users can sync data from Stripe by selecting one or more resources (Products, Customers, Charges, and Subscriptions). The sync process retrieves all records from Stripe in chunks of 100 until complete. The user can choose whether to run the process in the background or immediately.
Enhanced Pagination: By syncing all records, we now have the data needed to support any pagination mode and respect the user’s Nova configuration. Additionally, users can choose records per page: 25, 50, or 100.
Search: No more clicking "Next" until finding a record! Each resource includes search functionality for relevant fields, such as "ID," "Name," and "Email" in Customers.
Sorting: Sort Charges by amount, Customers by email, and more.
Filters: Filter Products by "active" or "inactive," Charges by date, and more.
Improved List and Detail Views: I focused on displaying the most relevant information in a way that closely resembles Stripe's Dashboard, ensuring the data is presented clearly (no printed out JSON strings!).
Relationships: Resource details are interconnected for easy navigation. For example, view a Customer's Charges and Subscriptions directly on their detail page, or navigate from a Charge to the associated Customer.
White Label: We avoid mentions of Nova (the menu reads "Stripe" instead of "Nova Stripe"), as some developers do not disclose the name of the dashboard to their clients by changing the footer, logo, URL, etc.
The previous version only supported Charges and Customers. This version introduces two new resources:
Screenshots
Click here to expand
Welcome Dialog
![image](https://private-user-images.githubusercontent.com/3766839/408660148-7a19ea34-0112-443a-aa29-1547f0da37e4.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg5NTU5NzEsIm5iZiI6MTczODk1NTY3MSwicGF0aCI6Ii8zNzY2ODM5LzQwODY2MDE0OC03YTE5ZWEzNC0wMTEyLTQ0M2EtYWEyOS0xNTQ3ZjBkYTM3ZTQucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIwNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMDdUMTkxNDMxWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9NjcxZjU3MmE2ZDcyNzI5MGY3Y2Q4MTQ3YmJkMTZmZmRhYmI4ZWIyOThlMTA1ZWZmNzE0OTNlYTNhYzJkN2I4MiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.3fdL5A9kQm2QZ9j8iJesZ75wIKB0efnityCsu8BhRxo)
Sync With Stripe
![image](https://private-user-images.githubusercontent.com/3766839/408624490-9a71ecbc-65b1-433c-8fd8-0b12e614a681.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg5NTU5NzEsIm5iZiI6MTczODk1NTY3MSwicGF0aCI6Ii8zNzY2ODM5LzQwODYyNDQ5MC05YTcxZWNiYy02NWIxLTQzM2MtOGZkOC0wYjEyZTYxNGE2ODEucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIwNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMDdUMTkxNDMxWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MWIwZmY3MTQ0ZTY0Mjg3NGE5YjFmNzFmYmU0ODMyMDUyOWYzZDY1Njg1YzdjMGRiYzgyYWE2NWE0MTI4YjI0NCZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.2_jJM2BLH0GlzgUMmzhAcb-jyspWyDmD3GDHlZTuWoM)
Enhanced Pagination
![image](https://private-user-images.githubusercontent.com/3766839/408624611-572df95c-e6e7-4387-bd30-c483d27ab782.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg5NTU5NzEsIm5iZiI6MTczODk1NTY3MSwicGF0aCI6Ii8zNzY2ODM5LzQwODYyNDYxMS01NzJkZjk1Yy1lNmU3LTQzODctYmQzMC1jNDgzZDI3YWI3ODIucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIwNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMDdUMTkxNDMxWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YjM4OTFiNDE5ZGI1NzM5NGE2MjFiZjY4MTI0OTVhYzI1OTNlMTE3YTA4Y2I0Njk4NDY1MmFmNDU1MzI4NzNiYiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.SUZ4eszdhpvORPx3F7P_sW1safh5-LoYhCVcWPpmFJw)
Search
![image](https://private-user-images.githubusercontent.com/3766839/408624777-53d40db4-a10a-4673-83c4-325855ee281e.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg5NTU5NzEsIm5iZiI6MTczODk1NTY3MSwicGF0aCI6Ii8zNzY2ODM5LzQwODYyNDc3Ny01M2Q0MGRiNC1hMTBhLTQ2NzMtODNjNC0zMjU4NTVlZTI4MWUucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIwNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMDdUMTkxNDMxWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MzQ0YTJhMWMxN2IyZWU3NWY2OWE4ODYyODQxN2NmMzUyNTA3NmI5MjQ2OWZmYTg1OTIyMTkwNjMxNGMxZjljYiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.wIUW5XS65deHf5HrnddLgDYtSkg2oiWnBOJfxnI7P3s)
Sorting
![image](https://private-user-images.githubusercontent.com/3766839/408624845-3e53031d-8ecb-47ad-908e-27befde8c835.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg5NTU5NzEsIm5iZiI6MTczODk1NTY3MSwicGF0aCI6Ii8zNzY2ODM5LzQwODYyNDg0NS0zZTUzMDMxZC04ZWNiLTQ3YWQtOTA4ZS0yN2JlZmRlOGM4MzUucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIwNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMDdUMTkxNDMxWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9ZDU1YTViMjNmNGI5YWVhYWVmZDc1MmUxOGZmOTM4MWQ5NjVhYmVjNDhmNmNjNTI3MDhhMGJhZTA3OTBkMmExNSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.hKvYyVtF0EXO26no2O9NgnNVMsPlbMxGV677X5OBGDs)
Filters and "Items Per Page" selector
![image](https://private-user-images.githubusercontent.com/3766839/408625029-717851d5-a21e-4caa-9398-66934ac8efa0.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg5NTU5NzEsIm5iZiI6MTczODk1NTY3MSwicGF0aCI6Ii8zNzY2ODM5LzQwODYyNTAyOS03MTc4NTFkNS1hMjFlLTRjYWEtOTM5OC02NjkzNGFjOGVmYTAucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIwNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMDdUMTkxNDMxWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9YWI1ZWE0MDZkODZlYTdkMDBiZDYyY2RhN2NlMTQzN2RkYThmMTU0MjM0YTQ3ODI2MWY3NjRjYTAxNDI1ZmRiMSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.c1ZC1Uh-FcOxiV8DDC1i0PFVeJFCfKTaspQReMmEdpM)
Improved List and Details Views
![image](https://private-user-images.githubusercontent.com/3766839/408625250-7abe423a-9a32-40f7-b465-fdc37f3e4776.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg5NTU5NzEsIm5iZiI6MTczODk1NTY3MSwicGF0aCI6Ii8zNzY2ODM5LzQwODYyNTI1MC03YWJlNDIzYS05YTMyLTQwZjctYjQ2NS1mZGMzN2YzZTQ3NzYucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIwNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMDdUMTkxNDMxWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MTExYzNlMzk1MzE1YjY1NGFkMzNkMjZkM2RmM2M2ZGFlNzQ5NDFlOTEwYTJkNTZmODNiN2QxNDM3Yzg1YmU1NSZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.0iNrUTW8ZqjJLPzO4YKLPfme1b05HidPsZpY-Z-hZh4)
Relationships
![rels](https://private-user-images.githubusercontent.com/3766839/408625346-cff67e2e-e440-4667-aab0-f4dfac581cb4.gif?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3Mzg5NTU5NzEsIm5iZiI6MTczODk1NTY3MSwicGF0aCI6Ii8zNzY2ODM5LzQwODYyNTM0Ni1jZmY2N2UyZS1lNDQwLTQ2NjctYWFiMC1mNGRmYWM1ODFjYjQuZ2lmP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIwNyUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMDdUMTkxNDMxWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MGQ2YmNhMGJiMGQyY2Y2NTJiMDhkYWQ5NTE5YmEzY2IzZDI4MmM5NWRlMjQ1NmY2MGM1NzI0ZTc2NGVlMWJmZiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.wYrv2nXXz_qzkKqnj1Iq-eqAdNte0p79cEEEZO0Npa0)
Motivation and Technical Details
Nova Stripe is a package for Laravel Nova that lets you view basic data from your Stripe account, such as charges and customers. It is a Nova tool. In Laravel Nova, tools are like plugins that integrate with Nova’s core and can add custom routes, views, components, and more. In terms of user interface, a tool provides a blank canvas where you can implement any user interface you need using Laravel, Inertia, and Vue.
The current version of Nova Stripe, version 2, uses Vue components to send requests to its API, which fetches data from Stripe via the Stripe PHP SDK and returns it to the front end. The data is then presented in a table or detail view.
This approach comes with limitations, reflected in the open issues.
Issues of the current version
Click here to read more
Most GitHub issues for Nova Stripe are enhancement requests, but a few are bug reports. One, in particular, is crucial to understanding the rest:
This issue highlights that Nova Stripe always uses "Previous" and "Next" buttons for pagination, regardless of the settings configured in Nova.
Laravel Nova offers three pagination modes for resource listings:
"simple": Default, with "Previous" and "Next" buttons.
"load-more": A "Load More" button at the bottom of the table.
"links": A list of numbered page links.
You can set the pagination style globally in the
config/nova.php
file:This configuration applies to all resource listings—but not to Nova Stripe, which always defaults to "simple" pagination with "Previous" and "Next" buttons.
Looking at the package code, the reason becomes clear: Nova Stripe doesn’t use the standard Vue components utilized by the rest of Laravel Nova. Instead, it relies on its own table component and pagination component. The latter has hardcoded "Previous" and "Next" links. In essence, it only supports the "simple" mode.
This reliance on custom components also accounts for another issue:
It also explains the absence of features requested in other GitHub issues marked as "enhancements":
And on the topic of issues, I’ve found two more myself:
Now, the key question is: why doesn’t Nova Stripe use the standard Nova components? Nova provides Vue components to support features like search, pagination modes, per-page limits, filters, sorting, and more. So, why not leverage them?
The answer is: those components are tightly coupled to Nova resources, and Nova resources are tightly coupled to Eloquent models. In other words:
$model
. The resource accesses the model’s fields, field types, relationships, etc., to perform queries and pass the data as props to the Vue page and child components.With this in mind, it’s clear why Nova Stripe doesn’t use the built-in Nova components: it can’t, because it doesn’t use Eloquent models. Instead, it’s simply a layer on top of API requests to Stripe.
Stripe's API caveats
On top of that, Stripe’s API has its caveats: for instance, when you request customers, it doesn’t return the total record count. It only returns the current page and a boolean indicating whether there are more records, but it doesn’t specify how many. The only way to know the total number of customers is to make sequential API calls until you've retrieved them all. If we had access to the total count, along with the number of records per page, we could implement a custom paginator. However, since this information is unavailable through Stripe’s API, the only choice is to use "Previous" and "Next" pagination.
Potential Solution 1: Expanding the custom components
Click here to read more
Now that we have a clear understanding of the scenario, let’s explore potential solutions.
One way to address these issues and limitations is to expand Nova Stripe’s custom Vue components to cover the missing features. This would involve creating new components to handle search, filters, sorting, etc. These components could be based on Nova’s core components, adapted to work with Stripe’s API. However, this approach would require a significant time investment, both for development and ongoing maintenance. Moreover, it doesn’t solve the pagination issue, given the limitations of Stripe’s API regarding total counts.
Initially, this seems like the only solution. If we were working with standard Nova resources and Eloquent models, all of our problems would be solved—we could take full advantage of the features that Nova provides out of the box. But Nova Stripe doesn’t use standard Eloquent models.
What if it did?
Potential Solution 2: Sync with Stripe (The approach chosen for this PR)
Click here to read more
Let's imagine, for a moment, that we have an Eloquent model for each of the objects we want to manage. For example, a
Customer
model would look something like this:We could then define the corresponding Nova resource and instantly gain access to all the features we need. Take this
Customer
resource class as an example:We mentioned that tools can register resources. They can also define routes and sidebar menus with links to those routes, like this:
A simple configuration like that can generate fully functional customer and charge resource pages, each complete with list and detail views, just like any other standard Nova resource—no need for custom components.
The question then becomes: how can we leverage Eloquent models if the data isn't stored in a database but is instead accessed via Stripe?
My solution is to sync the data from Stripe into a database.
Let's address some questions that might come up:
If we plan to save Stripe data into a database, we’ll need to decide on the database approach. Here are two options:
Both options are valid, but I chose sushi. With this in mind, this PR replicates the functionality of the Nova Stripe package in a completely new, rewritten-from-scratch version.
How to Test
.env
file:stripe
element to yourconfig/services.php
configuration file:NovaStripe
tool inapp/Providers/NovaServiceProvider
:php artisan queue:work
.Unit Testing and GitHub Actions
.txt
), in favor of a new action that installs PHP, checks out the code, runs the linter, and runs the tests.Please let me know if the changes to the actions are correct. Thank you!
Issues Fixed
Fixes #69
Fixes #67
Fixes #29
Fixes #22
Fixes #17
Fixes #12
Fixes #11
Fixes #10
Fixes #9
Note
All feedback welcome!