self::LIST_SLUG ), admin_url( 'admin.php' ) ) ); exit; } /** * Home: setup until it is done, then how the business is doing. */ public function render(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to manage subscriptions.', 'submex' ) ); } App_Host::start( self::SLUG ); Page_Shell::open( __( 'Home', 'submex' ), '', array(), '', false ); $this->render_test_result(); $this->render_product_notice(); if ( ! $this->setup->is_complete() ) { $this->render_checklist(); } if ( $this->setup->has_subscriptions() ) { $this->render_summary(); } Page_Shell::close(); App_Host::end(); } public function render_list(): void { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'You do not have permission to manage subscriptions.', 'submex' ) ); } App_Host::start( self::LIST_SLUG ); $this->render_list_screen(); App_Host::end(); } private function render_list_screen(): void { $id = absint( $_GET['subscription'] ?? 0 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $id ) { $this->render_detail( $id ); return; } $table = new Subscriptions_Table(); $table->prepare_items(); Page_Shell::open( __( 'All subscriptions', 'submex' ), __( 'Everyone who pays you on a schedule.', 'submex' ), array(), sprintf( '%s', esc_url( admin_url( 'post-new.php?post_type=product' ) ), esc_html__( 'New subscription product', 'submex' ) ) ); $this->render_bulk_notice(); if ( ! $this->setup->has_subscriptions() ) { printf( '

%s

%s

%s

', esc_html__( 'No subscriptions yet', 'submex' ), esc_html__( 'They will be listed here the moment someone buys a subscription product.', 'submex' ), esc_url( admin_url( 'admin.php?page=' . self::SLUG ) ), esc_html__( 'Finish setting up', 'submex' ) ); Page_Shell::close(); return; } $table->views(); echo '
'; printf( '', esc_attr( self::LIST_SLUG ) ); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- a read-only list filter; nothing is changed. if ( isset( $_GET['status'] ) ) { printf( '', esc_attr( sanitize_text_field( wp_unslash( $_GET['status'] ) ) ) ); } // phpcs:enable WordPress.Security.NonceVerification.Recommended $table->search_box( __( 'Search subscriptions', 'submex' ), 'submex-search' ); $table->display(); echo '
'; Page_Shell::close(); } private function render_test_result(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only. $result = isset( $_GET['submex_test'] ) ? sanitize_key( wp_unslash( $_GET['submex_test'] ) ) : ''; if ( '' === $result ) { return; } printf( '

%s

', esc_attr( Notices::feedback( 'pass' === $result ? 'success' : 'error', true ) ), 'pass' === $result ? esc_html__( 'Test renewal succeeded. A subscription was created, renewed and cleaned up — automatic billing works on this site.', 'submex' ) : esc_html__( 'The test renewal did not complete. Check that scheduled tasks are running, then try again.', 'submex' ) ); } private function render_checklist(): void { echo '

' . esc_html__( 'Get your first subscription running', 'submex' ) . '

    '; $number = 0; foreach ( $this->setup->steps() as $step ) { ++$number; $done = ! empty( $step['done'] ); printf( '
  1. %s
    ' . '%s%s%s', $done ? ' submex-step--done' : '', $done ? '✓' : esc_html( (string) $number ), esc_html( (string) $step['title'] ), esc_html( (string) $step['detail'] ), $step['action'] ? sprintf( '', esc_url( (string) $step['action']['url'] ), esc_html( (string) $step['action']['label'] ) ) : '' ); if ( 'create_product' === ( $step['form'] ?? '' ) ) { $this->render_product_form(); } echo '
  2. '; } echo '
'; } private function render_bulk_notice(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only. if ( ! isset( $_GET['submex_changed'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only. $changed = max( 0, (int) sanitize_text_field( wp_unslash( $_GET['submex_changed'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only. $asked = isset( $_GET['submex_asked'] ) ? max( 0, (int) sanitize_text_field( wp_unslash( $_GET['submex_asked'] ) ) ) : 0; $held = $asked - $changed; printf( '

%s%s

', esc_attr( Notices::feedback( $changed ? 'success' : 'warning', true ) ), esc_html( sprintf( /* translators: %d: number of subscriptions */ _n( '%d subscription updated.', '%d subscriptions updated.', $changed, 'submex' ), $changed ) ), $held > 0 ? ' ' . esc_html( sprintf( /* translators: %d: number of subscriptions left alone */ _n( '%d was left alone because that change is not allowed from its current status.', '%d were left alone because that change is not allowed from their current status.', $held, 'submex' ), $held ) ) : '' ); } private function render_product_notice(): void { $result = isset( $_GET['submex_product'] ) ? sanitize_key( wp_unslash( $_GET['submex_product'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended $message = match ( $result ) { 'created' => __( 'Your subscription product is published and ready to sell.', 'submex' ), 'invalid' => __( 'Check the name, the price and the interval: the price must be above zero and the interval a whole number of periods.', 'submex' ), 'failed' => __( 'The product could not be created.', 'submex' ), default => '', }; if ( '' !== $message ) { printf( '

%s

', esc_attr( Notices::feedback( 'created' === $result ? 'success' : 'error', true ) ), esc_html( $message ) ); } } /** * Creating the first product here rather than sending the merchant to find the * Product data panel, which is the step people get stuck on. */ private function render_product_form(): void { ?>
self::LIST_SLUG ), admin_url( 'admin.php' ) ); /* translators: %d: subscription ID */ $heading = sprintf( __( 'Subscription #%d', 'submex' ), $id ); // The heading below carries the status pill, so the shell's is for screen readers. Page_Shell::open( $heading, '', array( array( 'label' => __( 'All subscriptions', 'submex' ), 'url' => $back, ), array( 'label' => '#' . $id ), ), '', false ); printf( '

%s %s

← %s

', esc_html( $heading ), esc_attr( (string) $subscription->get_status() ), esc_html( $state['label'] ), esc_url( $back ), esc_html__( 'All subscriptions', 'submex' ) ); echo ''; $this->row( __( 'Customer', 'submex' ), trim( $subscription->get_billing_first_name() . ' ' . $subscription->get_billing_last_name() ) ?: (string) $subscription->get_billing_email() ); $this->row( __( 'Recurring total', 'submex' ), wp_strip_all_tags( $subscription->get_formatted_order_total() ) ); $this->row( __( 'Billing', 'submex' ), sprintf( '%d / %s', $subscription->get_billing_interval(), $subscription->get_billing_period() ) ); $this->row( __( 'Next payment', 'submex' ), (string) $subscription->get_next_payment() ?: '-' ); $this->pending_row( $subscription ); $this->row( __( 'Trial ends', 'submex' ), (string) $subscription->get_trial_end() ?: '-' ); $this->row( __( 'Payment method', 'submex' ), $subscription->get_payment_method_title() ?: (string) $subscription->get_payment_method() ); $this->row( __( 'Parent order', 'submex' ), $subscription->get_parent_order_id() ? '#' . $subscription->get_parent_order_id() : '-' ); echo '
'; $this->render_process_button( $subscription ); $this->render_activity( $id ); /** * Renders on the subscription detail screen, below its facts. * * @param Subscription $subscription */ do_action( 'submex_admin_subscription_detail', $subscription ); Page_Shell::close(); } /** * The most useful support tool in the plugin, and the most dangerous button - it * charges a real customer, so it names the amount and asks first. */ private function render_process_button( Subscription $subscription ): void { // Processing would only resume the charge already waiting on the payment provider. if ( Status_Presenter::pending_charge( $subscription ) ) { return; } $url = wp_nonce_url( add_query_arg( array( 'action' => 'submex_process_renewal', 'subscription' => $subscription->get_id(), ), admin_url( 'admin-post.php' ) ), 'submex_process_renewal_' . $subscription->get_id() ); $confirm = sprintf( /* translators: %s: recurring total */ __( 'This charges the customer %s right now. Continue?', 'submex' ), wp_strip_all_tags( $subscription->get_formatted_order_total() ) ); printf( '

%s

', esc_url( $url ), esc_attr( wp_json_encode( $confirm ) ), esc_html__( 'Process renewal now', 'submex' ) ); } /** * Recurring revenue at a glance, above the list. */ private function render_summary(): void { $stats = \Submex\Plugin::instance()->get( 'stats' ); if ( ! $stats instanceof \Submex\Data\Stats ) { return; } $mrr = $stats->mrr(); $history = $stats->history( 31 ); $oldest = $history ? reset( $history ) : null; $change = $oldest && isset( $oldest['mrr'] ) && $oldest['mrr'] > 0 ? round( ( ( $mrr->minor() - $oldest['mrr'] ) / $oldest['mrr'] ) * 100, 1 ) : null; echo '
'; printf( '
%s%s%s
', esc_html__( 'Monthly recurring revenue', 'submex' ), wp_kses_post( $mrr->format() ), null === $change ? '' . esc_html__( 'Tracking starts today', 'submex' ) . '' : sprintf( '%s%s%% %s', $change < 0 ? 'down' : 'up', $change < 0 ? '' : '+', esc_html( (string) $change ), esc_html__( 'over 30 days', 'submex' ) ) ); printf( '
%s%s
', esc_html__( 'Live subscriptions', 'submex' ), esc_html( number_format_i18n( $stats->active_count() ) ) ); $excluded = $stats->excluded_currency_count(); if ( $excluded ) { printf( '
%s%s%s
', esc_html__( 'Not counted', 'submex' ), esc_html( number_format_i18n( $excluded ) ), esc_html__( 'in another currency', 'submex' ) ); } echo '
'; } private function render_activity( int $id ): void { $entries = $this->activity->for_subscription( $id, 30 ); echo '

' . esc_html__( 'Activity', 'submex' ) . '

'; if ( empty( $entries ) ) { echo '

' . esc_html__( 'Nothing recorded yet.', 'submex' ) . '

'; return; } echo ''; foreach ( $entries as $entry ) { printf( '', esc_html( $entry->created_gmt ), esc_html( $entry->message ), esc_html( $entry->actor ) ); } echo '
%s%s%s
'; } public function process_renewal_now(): void { $id = absint( $_GET['subscription'] ?? 0 ); if ( ! current_user_can( self::CAPABILITY ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'submex_process_renewal_' . $id ) ) { wp_die( esc_html__( 'That request could not be verified.', 'submex' ) ); } $this->processor->process( $id ); wp_safe_redirect( add_query_arg( array( 'page' => self::LIST_SLUG, 'subscription' => $id, 'processed' => 1, ), admin_url( 'admin.php' ) ) ); exit; } private function pending_row( Subscription $subscription ): void { $pending = Status_Presenter::pending_charge( $subscription ); if ( ! $pending ) { return; } printf( '%s%s', esc_html__( 'Payment processing', 'submex' ), sprintf( /* translators: 1: renewal order number, linked, 2: how long ago it was submitted, such as "3 days" */ esc_html__( 'Renewal order %1$s was submitted %2$s ago and is waiting for the payment provider to confirm it.', 'submex' ), sprintf( '#%s', esc_url( $pending['order']->get_edit_order_url() ), esc_html( $pending['order']->get_order_number() ) ), esc_html( human_time_diff( $pending['since'] ) ) ) ); } private function row( string $label, string $value ): void { printf( '%s%s', esc_html( $label ), esc_html( $value ) ); } }