-
Notifications
You must be signed in to change notification settings - Fork 21
Fix: Add a new button for Stripe integration to "Fetch Products” in t… #417
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
base: develop
Are you sure you want to change the base?
Changes from all commits
35150e5
a6df7b7
b2b5abb
e5476ee
008e4da
d35c2a7
babf7ee
4fa4f00
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -45,6 +45,7 @@ public function init() { | |||||||
| add_action( 'update_option_wpum_settings', array( $this, 'flush_product_cache' ) ); | ||||||||
| add_action( 'wp_ajax_wpum_stripe_connect_account_info', array( $this, 'stripe_connect_account_info_ajax_response' ) ); | ||||||||
| add_action( 'admin_init', array( $this, 'handle_stripe_connect_disconnect' ) ); | ||||||||
| add_action( 'admin_init', array( $this, 'handle_fetch_stripe_products' ) ); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
|
|
@@ -256,11 +257,22 @@ public function register_settings( $settings ) { | |||||||
| 'type' => 'hidden', | ||||||||
| ); | ||||||||
|
|
||||||||
| $fetch_products_url = add_query_arg( | ||||||||
| array( | ||||||||
| 'page' => 'wpum-settings', | ||||||||
| 'fetch-products' => true, | ||||||||
| ), | ||||||||
| admin_url( 'users.php' ) | ||||||||
| ); | ||||||||
|
|
||||||||
| $fetch_products_url = wp_nonce_url( $fetch_products_url, 'wpum-stripe-fetch-products' ); | ||||||||
| $fetch_product_btn = sprintf( '<p><a href="%s#/stripe" class="button button-secondary">Fetch Stripe Products</a></p>', esc_url( $fetch_products_url ) ); | ||||||||
|
|
||||||||
| if ( $this->products && $this->products->totalRecurringProducts() > 1 ) { | ||||||||
| $settings['stripe'][] = array( | ||||||||
| 'id' => 'test_stripe_products', | ||||||||
| 'name' => __( 'Eligible Products', 'wp-user-manager' ), | ||||||||
| 'desc' => sprintf( 'Select the product prices users can subscribe to on the account page. This should be the same as the products defined in the <a target="_blank" href="%s">Stripe Customer Portal Subscription settings</a>.', 'https://wpusermanager.com/article/337-recurring-subscriptions/#configure-eligible-products' ), | ||||||||
| 'desc' => sprintf( 'Select the product prices users can subscribe to on the account page. This should be the same as the products defined in the <a target="_blank" href="%s">Stripe Customer Portal Subscription settings</a>. %s', 'https://wpusermanager.com/article/337-recurring-subscriptions/#configure-eligible-products ', $fetch_product_btn ), | ||||||||
| 'type' => 'multiselect', | ||||||||
| 'multiple' => true, | ||||||||
| 'options' => $this->products->get_plans(), | ||||||||
|
|
@@ -280,7 +292,7 @@ public function register_settings( $settings ) { | |||||||
| $settings['stripe'][] = array( | ||||||||
| 'id' => 'live_stripe_products', | ||||||||
| 'name' => __( 'Eligible Products', 'wp-user-manager' ), | ||||||||
| 'desc' => sprintf( 'Select the product prices users can subscribe to on the account page. This should be the same as the products defined in the <a target="_blank" href="%s">Stripe Customer Portal Subscription settings</a>.', 'https://wpusermanager.com/article/337-recurring-subscriptions/#configure-eligible-products' ), | ||||||||
| 'desc' => sprintf( 'Select the product prices users can subscribe to on the account page. This should be the same as the products defined in the <a target="_blank" href="%s">Stripe Customer Portal Subscription settings</a>. %s', 'https://wpusermanager.com/article/337-recurring-subscriptions/#configure-eligible-products', $fetch_product_btn ), | ||||||||
| 'type' => 'multiselect', | ||||||||
| 'multiple' => true, | ||||||||
| 'options' => $this->products->get_plans(), | ||||||||
|
|
@@ -602,4 +614,58 @@ public function handle_stripe_connect_disconnect() { | |||||||
|
|
||||||||
| return wp_safe_redirect( esc_url_raw( $redirect ) ); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Fetch Stripe Products | ||||||||
| */ | ||||||||
| public function handle_fetch_stripe_products() { | ||||||||
| $page = filter_input( INPUT_GET, 'page', FILTER_UNSAFE_RAW ); | ||||||||
| $page = sanitize_text_field( $page ); | ||||||||
|
|
||||||||
| if ( empty( $page ) ) { | ||||||||
| return; | ||||||||
| } | ||||||||
|
|
||||||||
| if ( 'wpum-settings' !== $page ) { | ||||||||
| return; | ||||||||
| } | ||||||||
|
|
||||||||
| $fetch_products = filter_input( INPUT_GET, 'fetch-products', FILTER_UNSAFE_RAW ); | ||||||||
| $fetch_products = sanitize_text_field( $fetch_products ); | ||||||||
|
|
||||||||
| if ( empty( $fetch_products ) ) { | ||||||||
| return; | ||||||||
| } | ||||||||
|
|
||||||||
| // Current user cannot handle this request, bail. | ||||||||
| if ( ! current_user_can( 'manage_options' ) ) { | ||||||||
| return; | ||||||||
| } | ||||||||
|
|
||||||||
| $nonce = filter_input( INPUT_GET, '_wpnonce', FILTER_UNSAFE_RAW ); | ||||||||
| $nonce = sanitize_text_field( $nonce ); | ||||||||
|
|
||||||||
| if ( empty( $nonce ) ) { | ||||||||
| return; | ||||||||
| } | ||||||||
|
|
||||||||
| if ( ! wp_verify_nonce( $nonce, 'wpum-stripe-fetch-products' ) ) { | ||||||||
| return; | ||||||||
| } | ||||||||
|
|
||||||||
| // Clear product cache | ||||||||
| $this->flush_product_cache(); | ||||||||
|
|
||||||||
| $products = new Products( $this->connect->get_stripe_secret(), $this->connect->get_gateway_mode() ); | ||||||||
| $products->all( true ); | ||||||||
|
|
||||||||
|
Comment on lines
+656
to
+661
|
||||||||
| $redirect = remove_query_arg( | ||||||||
| array( | ||||||||
| '_wpnonce', | ||||||||
| 'fetch-products', | ||||||||
| ) | ||||||||
| ); | ||||||||
|
||||||||
| ); | |
| ); | |
| $redirect .= '#/stripe'; |
Copilot
AI
Feb 27, 2026
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.
wp_safe_redirect() does not terminate execution. To avoid any additional processing after sending the Location header (and to match common WP redirect patterns), call exit immediately after the redirect.
| return wp_safe_redirect( esc_url_raw( $redirect ) ); | |
| wp_safe_redirect( esc_url_raw( $redirect ) ); | |
| exit; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| use WPUserManager\Stripe\Controllers\Subscriptions; | ||
| use WPUserManager\Stripe\Models\Product; | ||
| use WPUserManager\Stripe\Models\User; | ||
| use WPUserManager\Stripe\Controllers\Products; | ||
|
|
||
| /** | ||
| * StripeWebhookController | ||
|
|
@@ -38,6 +39,16 @@ class StripeWebhookController { | |
| */ | ||
| protected $invoices; | ||
|
|
||
| /** | ||
| * @var string | ||
| */ | ||
| protected $gateway_mode; | ||
|
|
||
| /** | ||
| * @var string | ||
| */ | ||
| protected $secret_key; | ||
|
|
||
| /** | ||
| * StripeWebhookController constructor. | ||
| * | ||
|
|
@@ -53,6 +64,8 @@ public function __construct( $secret_key, $webhook_secret, $gateway_mode ) { | |
| $this->subscriptions = new Subscriptions( $gateway_mode ); | ||
| $this->invoices = new Invoices( $gateway_mode ); | ||
| $this->webhook_secret = $webhook_secret; | ||
| $this->gateway_mode = $gateway_mode; | ||
| $this->secret_key = $secret_key; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -310,4 +323,55 @@ protected function handleInvoicePaymentSucceeded( $payload ) { | |
|
|
||
| return new \WP_REST_Response( 'Webhook handled', 200 ); | ||
| } | ||
|
|
||
| /** | ||
| * Handle the product.created webhook to update the cached Stripe products. | ||
| * | ||
| * @param array $payload | ||
| * | ||
| * @return \WP_REST_Response | ||
| * @throws \Exception | ||
| */ | ||
| protected function handleProductCreated( $payload ) { | ||
| $products = new Products( $this->secret_key, $this->gateway_mode ); | ||
| $products->all( true ); | ||
|
|
||
| do_action( 'wpum_stripe_webhook_product_updated', $payload ); | ||
|
|
||
| return new \WP_REST_Response( 'Webhook handled', 200 ); | ||
| } | ||
|
Comment on lines
+335
to
+342
|
||
|
|
||
| /** | ||
| * Handle the product.deleted webhook to update the cached Stripe products. | ||
| * | ||
| * @param array $payload | ||
| * | ||
| * @return \WP_REST_Response | ||
| * @throws \Exception | ||
| */ | ||
| protected function handleProductDeleted( $payload ) { | ||
| $products = new Products( $this->secret_key, $this->gateway_mode ); | ||
| $products->all( true ); | ||
|
|
||
| do_action( 'wpum_stripe_webhook_product_updated', $payload ); | ||
|
|
||
| return new \WP_REST_Response( 'Webhook handled', 200 ); | ||
| } | ||
|
|
||
| /** | ||
| * Handle the product.updated webhook to update the cached Stripe products. | ||
| * | ||
| * @param array $payload | ||
| * | ||
| * @return \WP_REST_Response | ||
| * @throws \Exception | ||
| */ | ||
| protected function handleProductUpdated( $payload ) { | ||
| $products = new Products( $this->secret_key, $this->gateway_mode ); | ||
| $products->all( true ); | ||
|
|
||
| do_action( 'wpum_stripe_webhook_product_updated', $payload ); | ||
|
|
||
| return new \WP_REST_Response( 'Webhook handled', 200 ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| import { test, expect, wpAdminLogin, wpCli, createUser, deleteUser } from './fixtures'; | ||
| import { | ||
| isStripeConfigured, | ||
| installTestBillingOverride, | ||
| removeTestBillingOverride, | ||
| configureStripeSettings, | ||
| createStripeTestProduct, | ||
| deleteStripeTestProduct, | ||
| } from './helpers/stripe'; | ||
|
|
||
| let testProductId: string; | ||
| let testPriceId: string; | ||
|
|
||
| test.describe('Stripe Fetch Products', () => { | ||
| test.beforeAll(async () => { | ||
| if (!isStripeConfigured()) { | ||
| test.skip(); | ||
| return; | ||
| } | ||
|
|
||
| installTestBillingOverride(); | ||
|
|
||
| const { productId, priceId } = createStripeTestProduct(); | ||
| testProductId = productId; | ||
| testPriceId = priceId; | ||
|
|
||
| configureStripeSettings( | ||
| process.env.STRIPE_PUBLISHABLE_KEY!, | ||
| process.env.STRIPE_SECRET_KEY!, | ||
| process.env.STRIPE_WEBHOOK_SECRET || '', | ||
| testPriceId | ||
| ); | ||
| }); | ||
|
|
||
| test.afterAll(async () => { | ||
| if (!isStripeConfigured()) return; | ||
|
|
||
| removeTestBillingOverride(); | ||
|
|
||
| if (testProductId) { | ||
| deleteStripeTestProduct(testProductId); | ||
| } | ||
|
|
||
| deleteUser('stripe_e2e_subscriber'); | ||
| deleteUser('stripe_e2e_subscriber@example.com'); | ||
| }); | ||
|
|
||
| test('Fetch Stripe Products button appears on settings page', async ({ page }) => { | ||
| await wpAdminLogin(page); | ||
| await page.goto('/wp-admin/users.php?page=wpum-settings#/stripe'); | ||
| await page.waitForLoadState('networkidle'); | ||
|
|
||
| // The settings page uses wp-optionskit Vue SPA — click the Stripe tab | ||
| const stripeTab = page.locator('a[href="#/stripe"]'); | ||
| if (await stripeTab.isVisible({ timeout: 3000 }).catch(() => false)) { | ||
| await stripeTab.click(); | ||
| await page.waitForTimeout(1000); | ||
| } | ||
|
|
||
| // The Fetch Stripe Products button should be visible (appears in both test & live sections) | ||
| const fetchButton = page.locator('a.button.button-secondary', { hasText: 'Fetch Stripe Products' }).first(); | ||
| await expect(fetchButton).toBeVisible({ timeout: 10000 }); | ||
| }); | ||
|
|
||
| test('Fetch button refreshes products and redirects back', async ({ page }) => { | ||
| await wpAdminLogin(page); | ||
| await page.goto('/wp-admin/users.php?page=wpum-settings#/stripe'); | ||
| await page.waitForLoadState('networkidle'); | ||
|
|
||
| // Navigate to Stripe tab | ||
| const stripeTab = page.locator('a[href="#/stripe"]'); | ||
| if (await stripeTab.isVisible({ timeout: 3000 }).catch(() => false)) { | ||
| await stripeTab.click(); | ||
| await page.waitForTimeout(1000); | ||
| } | ||
|
|
||
| // Click the Fetch button (appears in both test & live sections — use first) | ||
| const fetchButton = page.locator('a.button.button-secondary', { hasText: 'Fetch Stripe Products' }).first(); | ||
| await fetchButton.click(); | ||
|
|
||
| // Should redirect back to the settings page (nonce and fetch-products params removed) | ||
| await page.waitForURL(/page=wpum-settings/, { timeout: 15000 }); | ||
| expect(page.url()).not.toContain('fetch-products'); | ||
|
|
||
| // The settings page should still render correctly | ||
| const heading = page.locator('h1'); | ||
| await expect(heading).toContainText(/Settings/i, { timeout: 5000 }); | ||
| }); | ||
|
|
||
| test('non-admin user cannot fetch products', async ({ page }) => { | ||
| // Create a subscriber user | ||
| deleteUser('stripe_e2e_subscriber'); | ||
| createUser('stripe_e2e_subscriber', 'stripe_e2e_subscriber@example.com', 'TestPass123!', 'subscriber'); | ||
|
|
||
| // Log in as subscriber | ||
| await wpAdminLogin(page, 'stripe_e2e_subscriber', 'TestPass123!'); | ||
|
|
||
| // Try to access the fetch products URL directly | ||
| await page.goto('/wp-admin/users.php?page=wpum-settings&fetch-products=true'); | ||
|
|
||
| // Subscriber should not see the settings page — WordPress redirects to admin | ||
| // or shows "You do not have sufficient permissions" | ||
| const url = page.url(); | ||
| const content = await page.content(); | ||
| const blocked = | ||
| !url.includes('page=wpum-settings') || | ||
| content.includes('You need a higher level of permission') || | ||
| content.includes('Sorry, you are not allowed'); | ||
|
|
||
| expect(blocked).toBeTruthy(); | ||
| }); | ||
| }); |
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.
The documentation URL in this description has a trailing space at the end of the href string, which will be URL-encoded and can break the link target. Remove the trailing space so the anchor points to the intended section.