id ) {
return;
}
printf(
'
',
esc_html__( 'Submex is active.', 'submex' ),
esc_html__( 'Turn any product into a subscription in about two minutes.', 'submex' ),
esc_url( add_query_arg( array( 'page' => Menu::SLUG ), admin_url( 'admin.php' ) ) ),
esc_html__( 'Set up subscriptions', 'submex' ),
esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=submex_dismiss_setup' ), 'submex_dismiss_setup' ) ),
esc_html__( 'Dismiss', 'submex' )
);
}
public function dismiss(): void {
if ( current_user_can( Menu::CAPABILITY ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'submex_dismiss_setup' ) ) {
update_option( self::DISMISSED, 1, false );
}
wp_safe_redirect( wp_get_referer() ?: admin_url( 'plugins.php' ) );
exit;
}
/**
* @return array
*/
public function steps(): array {
$connected = $this->connected_gateways();
$product = $this->first_subscription_product();
$queue_ok = $this->scheduler->queue_is_healthy();
return array(
array(
'done' => (bool) $connected,
'title' => __( 'Connect a payment method', 'submex' ),
'detail' => $connected
/* translators: %s: comma-separated list of connected gateways */
? sprintf( __( '%s can charge renewals automatically.', 'submex' ), implode( ', ', $connected ) )
: __( 'Subscriptions renew by themselves once a gateway such as PayPal is connected. Without one, renewals become invoices the customer pays by hand, which still works.', 'submex' ),
'action' => array(
'label' => $connected ? __( 'Change', 'submex' ) : __( 'Choose a gateway', 'submex' ),
'url' => admin_url( 'admin.php?page=wc-settings&tab=submex' ),
),
),
array(
'done' => (bool) $product,
'title' => __( 'Create a subscription product', 'submex' ),
'detail' => $product
/* translators: %s: product name */
? sprintf( __( '"%s" is ready to sell.', 'submex' ), $product->get_name() )
: __( 'Edit any simple product and tick Subscription in the Product data panel.', 'submex' ),
'action' => $product
? array(
'label' => __( 'Edit', 'submex' ),
'url' => get_edit_post_link( $product->get_id(), '' ),
)
: null,
'form' => $product ? null : 'create_product',
),
array(
'done' => $queue_ok,
'title' => __( 'Check automatic renewals can run', 'submex' ),
'detail' => $queue_ok
? __( 'Renewals are processing normally.', 'submex' )
: __( 'Scheduled tasks are overdue. On a quiet site WordPress cron may not fire often enough — a real server cron fixes it.', 'submex' ),
'action' => null,
),
array(
'done' => (bool) get_option( 'submex_test_renewal_passed' ),
'title' => __( 'Run a test renewal', 'submex' ),
'detail' => __( 'See a renewal happen end to end, on a throwaway subscription. Nobody is charged and everything is deleted afterwards.', 'submex' ),
'action' => array(
'label' => __( 'Run test renewal', 'submex' ),
'url' => wp_nonce_url( admin_url( 'admin-post.php?action=submex_test_renewal' ), 'submex_test_renewal' ),
),
),
);
}
/**
* Make the first subscription product from the guide, so the merchant never has to
* find the Product data panel to get started.
*/
public function create_first_product(): void {
if ( ! current_user_can( Menu::CAPABILITY ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'submex_create_product' ) ) {
wp_die( esc_html__( 'That request could not be verified.', 'submex' ) );
}
$name = sanitize_text_field( wp_unslash( $_POST['submex_name'] ?? '' ) );
$price = wc_format_decimal( sanitize_text_field( wp_unslash( $_POST['submex_price'] ?? '' ) ) );
$period = sanitize_key( wp_unslash( $_POST['submex_period'] ?? 'month' ) );
$period = in_array( $period, array( 'day', 'week', 'month', 'year' ), true ) ? $period : 'month';
$interval = intval( wp_unslash( $_POST['submex_interval'] ?? 1 ) );
$trial = intval( wp_unslash( $_POST['submex_trial'] ?? 0 ) );
// intval with a range check, not absint: absint would turn a posted -4 into 4.
if ( '' === $name || '' === $price || (float) $price <= 0 || $interval < 1 || $interval > 365 || $trial < 0 || $trial > 365 ) {
$this->redirect_back( 'invalid' );
}
$product = new \WC_Product_Simple();
$product->set_name( $name );
$product->set_regular_price( $price );
$product->set_status( 'publish' );
$product->update_meta_data( Subscription_Product::META_ENABLED, 'yes' );
$product->update_meta_data( Subscription_Product::META_PERIOD, $period );
$product->update_meta_data( Subscription_Product::META_INTERVAL, $interval );
$product->update_meta_data( Subscription_Product::META_TRIAL_DAYS, $trial );
$product->save();
$this->redirect_back( $product->get_id() ? 'created' : 'failed' );
}
private function redirect_back( string $result ): void {
wp_safe_redirect( add_query_arg( 'submex_product', $result, admin_url( 'admin.php?page=' . Menu::SLUG ) ) );
exit;
}
/**
* Create a throwaway subscription, renew it, then delete the lot.
*
* The most useful control in the plugin: a merchant who has watched a renewal happen
* will trust it with real customers, and one who hasn't opens a support ticket.
*/
public function run_test_renewal(): void {
if ( ! current_user_can( Menu::CAPABILITY ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'submex_test_renewal' ) ) {
wp_die( esc_html__( 'That request could not be verified.', 'submex' ) );
}
$result = $this->perform_test_renewal();
wp_safe_redirect(
add_query_arg(
array(
'page' => Menu::SLUG,
'submex_test' => $result ? 'pass' : 'fail',
),
admin_url( 'admin.php' )
)
);
exit;
}
public function perform_test_renewal(): bool {
Test_Gateway::sanction();
$processor = \Submex\Plugin::instance()->get( 'processor' );
$registry = \Submex\Plugin::instance()->get( 'gateways' );
if ( ! $processor instanceof Renewal_Processor || ! $registry ) {
return false;
}
$registry->add( new Test_Gateway() );
$subscription = new Subscription();
$subscription->set_currency( get_woocommerce_currency() );
$subscription->set_billing_period( 'month' );
$subscription->set_billing_interval( 1 );
$subscription->set_payment_method( Test_Gateway::ID );
$subscription->set_payment_method_title( __( 'Submex self-test', 'submex' ) );
$subscription->set_next_payment( gmdate( 'Y-m-d H:i:s', time() - MINUTE_IN_SECONDS ) );
$subscription->update_meta_data( '_submex_site_url', get_option( 'siteurl' ) );
$subscription->update_meta_data( '_submex_is_test', 'yes' );
$subscription->transition_to( Subscription_Status::Active );
$subscription->save();
$id = $subscription->get_id();
$scripted = wc_get_order( $id );
if ( ! $scripted instanceof Subscription ) {
return false;
}
Test_Gateway::script_subscription( $scripted, array( 'success' ) );
$processor->process( $id );
$renewed = ! empty(
array_filter(
$this->slots_for( $id ),
static fn( $slot ): bool => 'paid' === $slot->state
)
);
$this->cleanup_test( $id );
update_option( 'submex_test_renewal_passed', $renewed ? 1 : 0, false );
return $renewed;
}
/**
* @return object[]
*/
private function slots_for( int $id ): array {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
return $wpdb->get_results( $wpdb->prepare( "SELECT state FROM {$wpdb->prefix}submex_charge_slot WHERE subscription_id = %d", $id ) );
}
private function cleanup_test( int $id ): void {
global $wpdb;
foreach ( wc_get_orders(
array(
'limit' => -1,
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- renewal orders are linked to their subscription by meta.
'meta_key' => '_submex_subscription_id',
'meta_value' => $id,
// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
)
) as $order ) {
$order->delete( true );
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete( $wpdb->prefix . 'submex_charge_slot', array( 'subscription_id' => $id ), array( '%d' ) );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete( $wpdb->prefix . 'submex_activity', array( 'subscription_id' => $id ), array( '%d' ) );
$subscription = wc_get_order( $id );
if ( $subscription ) {
$subscription->delete( true );
}
}
/**
* Which gateways can actually charge a renewal right now.
*
* Asks the registry rather than naming one, so a gateway added by Pro counts and the
* checklist never tells a shop using another gateway to go and connect PayPal.
*
* @return string[]
*/
private function connected_gateways(): array {
$registry = \Submex\Plugin::instance()->get( 'gateways' );
if ( ! $registry instanceof \Submex\Gateways\Gateway_Registry ) {
return array();
}
$names = array();
foreach ( $registry->all() as $gateway ) {
// Manual and the test gateway are always present and charge nobody by
// themselves, so counting them would tick this step on a store with nothing set up.
if ( in_array( $gateway->id(), array( 'submex_manual', Test_Gateway::ID ), true ) ) {
continue;
}
$names[] = $gateway->title();
}
return $names;
}
private function first_subscription_product(): ?\WC_Product {
$products = wc_get_products(
array(
'limit' => 1,
'status' => 'publish',
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- limit 1, once while setting up.
'meta_key' => Subscription_Product::META_ENABLED,
'meta_value' => 'yes',
// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_key, WordPress.DB.SlowDBQuery.slow_db_query_meta_value
)
);
return $products[0] ?? null;
}
public function is_complete(): bool {
foreach ( $this->steps() as $step ) {
if ( ! $step['done'] ) {
return false;
}
}
return true;
}
public function has_subscriptions(): bool {
return ! empty( Subscription_Query::ids( array( 'limit' => 1 ) ) );
}
}