'United States', 'GB' => 'United Kingdom', 'CA' => 'Canada', 'AU' => 'Australia', 'AT' => 'Austria', 'BE' => 'Belgium', 'BR' => 'Brazil', 'BG' => 'Bulgaria', 'HR' => 'Croatia', 'CY' => 'Cyprus', 'CZ' => 'Czech Republic', 'DK' => 'Denmark', 'EE' => 'Estonia', 'FI' => 'Finland', 'FR' => 'France', 'DE' => 'Germany', 'GR' => 'Greece', 'HK' => 'Hong Kong', 'HU' => 'Hungary', 'IE' => 'Ireland', 'IT' => 'Italy', 'JP' => 'Japan', 'LV' => 'Latvia', 'LT' => 'Lithuania', 'LU' => 'Luxembourg', 'MY' => 'Malaysia', 'MT' => 'Malta', 'MX' => 'Mexico', 'NL' => 'Netherlands', 'NZ' => 'New Zealand', 'NO' => 'Norway', 'PL' => 'Poland', 'PT' => 'Portugal', 'RO' => 'Romania', 'SG' => 'Singapore', 'SK' => 'Slovakia', 'SI' => 'Slovenia', 'ES' => 'Spain', 'SE' => 'Sweden', 'CH' => 'Switzerland', 'TH' => 'Thailand', 'AE' => 'United Arab Emirates', ); } function acta_get_supported_currencies() { return array( 'usd' => 'USD - US Dollar', 'eur' => 'EUR - Euro', 'gbp' => 'GBP - British Pound', 'cad' => 'CAD - Canadian Dollar', 'aud' => 'AUD - Australian Dollar', 'jpy' => 'JPY - Japanese Yen', 'chf' => 'CHF - Swiss Franc', 'nzd' => 'NZD - New Zealand Dollar', 'sek' => 'SEK - Swedish Krona', 'nok' => 'NOK - Norwegian Krone', 'dkk' => 'DKK - Danish Krone', 'sgd' => 'SGD - Singapore Dollar', 'hkd' => 'HKD - Hong Kong Dollar', 'mxn' => 'MXN - Mexican Peso', 'brl' => 'BRL - Brazilian Real', 'pln' => 'PLN - Polish Zloty', 'czk' => 'CZK - Czech Koruna', 'huf' => 'HUF - Hungarian Forint', 'ron' => 'RON - Romanian Leu', 'bgn' => 'BGN - Bulgarian Lev', 'thb' => 'THB - Thai Baht', 'myr' => 'MYR - Malaysian Ringgit', 'aed' => 'AED - UAE Dirham', ); } // ─── Activation: generate a secret key ─────────────────────────────────────── register_activation_hook( __FILE__, 'acta_activate' ); function acta_activate() { if ( ! get_option( ACTA_OPTION_KEY ) ) { update_option( ACTA_OPTION_KEY, bin2hex( random_bytes( 32 ) ) ); } // Initialize connection status if not set if ( ! get_option( ACTA_CONNECTION_STATUS ) ) { update_option( ACTA_CONNECTION_STATUS, 'not_registered' ); } // Redirect to the Acta settings page after activation. update_option( ACTA_ACTIVATION_REDIRECT_OPTION, 1 ); } // Also redirect after plugin update (upload zip) — WordPress shows "Go to Plugin Installer" link otherwise. add_action( 'upgrader_process_complete', 'acta_maybe_set_redirect_after_update', 10, 2 ); function acta_maybe_set_redirect_after_update( $upgrader_object, $options ) { if ( empty( $options['action'] ) || $options['action'] !== 'update' ) { return; } if ( empty( $options['type'] ) || $options['type'] !== 'plugin' ) { return; } $our_plugin = plugin_basename( __FILE__ ); $updated = array(); if ( ! empty( $options['plugins'] ) && is_array( $options['plugins'] ) ) { $updated = $options['plugins']; } elseif ( ! empty( $options['plugin'] ) ) { $updated = array( $options['plugin'] ); } if ( in_array( $our_plugin, $updated, true ) ) { update_option( ACTA_ACTIVATION_REDIRECT_OPTION, 1 ); } } add_action( 'admin_init', 'acta_maybe_redirect_after_activation' ); function acta_maybe_redirect_after_activation() { if ( ! get_option( ACTA_ACTIVATION_REDIRECT_OPTION ) ) { return; } delete_option( ACTA_ACTIVATION_REDIRECT_OPTION ); if ( ! is_admin() || ! current_user_can( 'manage_options' ) || wp_doing_ajax() ) { return; } if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return; } if ( isset( $_GET['activate-multi'] ) ) { return; } wp_safe_redirect( admin_url( 'admin.php?page=' . ACTA_ADMIN_PAGE_SLUG ) ); exit; } // ─── Admin settings page ───────────────────────────────────────────────────── add_action( 'admin_menu', 'acta_add_admin_menu' ); add_action( 'admin_enqueue_scripts', 'acta_enqueue_admin_scripts' ); function acta_enqueue_admin_scripts( $hook ) { if ( 'toplevel_page_' . ACTA_ADMIN_PAGE_SLUG !== $hook ) { return; } $asset_url = plugin_dir_url( __FILE__ ) . 'assets/js/admin-copy-price.js'; wp_enqueue_script( 'acta-admin-copy-price', $asset_url, array(), ACTA_PLUGIN_VERSION, true ); } function acta_add_admin_menu() { add_menu_page( 'Acta Settings', 'Acta', 'manage_options', ACTA_ADMIN_PAGE_SLUG, 'acta_settings_page', ACTA_MENU_ICON, 30 ); } add_action( 'admin_init', 'acta_settings_init' ); function acta_settings_init() { register_setting( 'acta_settings', ACTA_OPTION_KEY, array( 'sanitize_callback' => 'sanitize_text_field', ) ); register_setting( 'acta_settings', ACTA_PUBLISHER_ID_KEY, array( 'sanitize_callback' => 'sanitize_text_field', ) ); } // ─── Backend API functions ─────────────────────────────────────────────────── /** * Self-service onboarding: create a new publisher on the Acta backend. * Called when publisher clicks "Get Started with Acta". * * @return array { success: bool, message: string, publisherId?: string, stripeOnboardingUrl?: string } */ function acta_onboard_to_backend( $email, $first_name, $last_name, $country, $currency, $article_price ) { $backend_url = rtrim( ACTA_BACKEND_URL, '/' ) . '/api/v1/public/onboard-wordpress'; $secret = get_option( ACTA_OPTION_KEY, '' ); $endpoint = rest_url( 'acta/v1/content' ); $response = wp_remote_post( $backend_url, array( 'timeout' => 30, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'email' => $email, 'firstName' => $first_name, 'lastName' => $last_name, 'website' => home_url(), 'name' => get_bloginfo( 'name' ), 'country' => $country, 'currency' => $currency, 'articlePrice' => (float) $article_price, 'pluginEndpoint' => $endpoint, 'pluginSecretKey'=> $secret, ) ), ) ); if ( is_wp_error( $response ) ) { return array( 'success' => false, 'message' => 'Connection failed: ' . $response->get_error_message(), ); } $code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code === 201 && ! empty( $body['success'] ) ) { return array( 'success' => true, 'message' => 'Publisher created successfully.', 'publisherId' => $body['publisherId'] ?? '', 'stripeOnboardingUrl'=> $body['stripeOnboardingUrl'] ?? '', ); } $error_msg = $body['error'] ?? ( 'HTTP ' . $code ); return array( 'success' => false, 'message' => $error_msg, ); } /** * Reconnect the plugin to an existing publisher on the Acta backend. * Used for plugin reinstall or v1 → v2 migration. */ function acta_connect_to_backend( $publisher_id, $plugin_endpoint, $plugin_secret_key ) { $backend_url = rtrim( ACTA_BACKEND_URL, '/' ) . '/api/v1/public/connect-wordpress'; $response = wp_remote_post( $backend_url, array( 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'publisherId' => $publisher_id, 'siteUrl' => home_url(), 'pluginEndpoint' => $plugin_endpoint, 'pluginSecretKey' => $plugin_secret_key, ) ), ) ); if ( is_wp_error( $response ) ) { return array( 'success' => false, 'message' => 'Connection failed: ' . $response->get_error_message(), ); } $code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code === 200 && ! empty( $body['success'] ) ) { return array( 'success' => true, 'message' => $body['message'] ?? 'Connected successfully.', ); } $error_msg = $body['error'] ?? ( 'HTTP ' . $code ); return array( 'success' => false, 'message' => $error_msg, ); } /** * Tell Acta this site is disconnecting — the plugin is being removed, or the * publisher pressed Disconnect. * * Acta marks the publisher inactive, which stops the paywall being served and * stops new purchases. Nothing is deleted: the account, Stripe details and any * balance owed are kept, so reinstalling picks up where this left off. * * @return array{ success: bool, message: string } */ function acta_disconnect_from_backend( $publisher_id, $secret, $site_url ) { if ( empty( $publisher_id ) || empty( $secret ) ) { return array( 'success' => false, 'message' => 'Not connected to Acta.', ); } $response = wp_remote_post( rtrim( ACTA_BACKEND_URL, '/' ) . '/api/v1/public/disconnect-wordpress', array( 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'publisherId' => $publisher_id, 'siteUrl' => $site_url, 'pluginSecretKey' => $secret, ) ), ) ); if ( is_wp_error( $response ) ) { return array( 'success' => false, 'message' => 'Connection failed: ' . $response->get_error_message(), ); } $code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 200 === $code && ! empty( $body['success'] ) ) { return array( 'success' => true, 'message' => 'Disconnected from Acta.', ); } return array( 'success' => false, 'message' => $body['error'] ?? ( 'HTTP ' . $code ), ); } /** * Fetch publisher info (article price, rate limit) from Acta backend. * * @return array{ articlePrice: float, changesRemaining: int, revShare?: int }|null */ function acta_fetch_publisher_info( $publisher_id, $secret, $site_url ) { if ( empty( $publisher_id ) || empty( $secret ) ) { return null; } $url = rtrim( ACTA_BACKEND_URL, '/' ) . '/api/v1/public/wordpress-publisher-info'; $response = wp_remote_post( $url, array( 'timeout' => 10, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'publisherId' => $publisher_id, 'key' => $secret, 'siteUrl' => $site_url, ) ), ) ); if ( is_wp_error( $response ) ) { return null; } $code = wp_remote_retrieve_response_code( $response ); if ( $code !== 200 ) { return null; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! is_array( $body ) || ! isset( $body['articlePrice'] ) ) { return null; } return array( 'articlePrice' => (float) $body['articlePrice'], 'changesRemaining' => isset( $body['changesRemaining'] ) ? (int) $body['changesRemaining'] : 3, 'revShare' => isset( $body['revShare'] ) ? (int) $body['revShare'] : 15, ); } /** * Update default article price via Acta backend. * * @return array{ success: bool, message: string, articlePrice?: float, changesRemaining?: int } */ function acta_update_default_price( $publisher_id, $secret, $site_url, $article_price ) { $url = rtrim( ACTA_BACKEND_URL, '/' ) . '/api/v1/public/wordpress-update-article-price'; $body = array( 'publisherId' => $publisher_id, 'key' => $secret, 'siteUrl' => $site_url, 'articlePrice' => (float) $article_price, ); $response = wp_remote_post( $url, array( 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $body ), ) ); if ( is_wp_error( $response ) ) { return array( 'success' => false, 'message' => $response->get_error_message(), ); } $code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code === 200 && ! empty( $body['success'] ) ) { return array( 'success' => true, 'message' => 'Default price updated.', 'articlePrice' => $body['articlePrice'] ?? $article_price, 'changesRemaining' => $body['changesRemaining'] ?? 0, ); } return array( 'success' => false, 'message' => $body['error'] ?? ( 'HTTP ' . $code ), ); } /** * Push the paywall mode (pay-per-article only vs. alongside subscriptions) to * the Acta backend. Stored on the publisher record and delivered to the embed * script at runtime — so the flag never lives in (cacheable) page HTML and a * toggle takes effect on the next page load without a cache purge. */ function acta_update_paywall_mode( $publisher_id, $secret, $site_url, $subscribe_override ) { $url = rtrim( ACTA_BACKEND_URL, '/' ) . '/api/v1/public/wordpress-update-paywall-mode'; $body = array( 'publisherId' => $publisher_id, 'key' => $secret, 'siteUrl' => $site_url, 'subscribeOverride' => (bool) $subscribe_override, ); $response = wp_remote_post( $url, array( 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( $body ), ) ); if ( is_wp_error( $response ) ) { return array( 'success' => false, 'message' => $response->get_error_message(), ); } $code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code === 200 && ! empty( $body['success'] ) ) { return array( 'success' => true ); } return array( 'success' => false, 'message' => $body['error'] ?? ( 'HTTP ' . $code ), ); } // ─── Settings page renderer ───────────────────────────────────────────────── function acta_settings_page() { $secret = get_option( ACTA_OPTION_KEY, '' ); $publisher_id = get_option( ACTA_PUBLISHER_ID_KEY, '' ); $stripe_url = get_option( ACTA_STRIPE_URL_KEY, '' ); $conn_status = get_option( ACTA_CONNECTION_STATUS, 'not_registered' ); $endpoint = rest_url( 'acta/v1/content' ); // ── Handle form actions ────────────────────────────────────────────────── // Handle "Get Started with Acta" (self-service onboarding) if ( isset( $_POST['acta_action'] ) && $_POST['acta_action'] === 'onboard' && check_admin_referer( 'acta_onboard' ) ) { $email = sanitize_email( wp_unslash( $_POST['acta_email'] ?? '' ) ); $first_name = sanitize_text_field( wp_unslash( $_POST['acta_first_name'] ?? '' ) ); $last_name = sanitize_text_field( wp_unslash( $_POST['acta_last_name'] ?? '' ) ); $country = sanitize_text_field( wp_unslash( $_POST['acta_country'] ?? '' ) ); $currency = sanitize_text_field( wp_unslash( $_POST['acta_currency'] ?? '' ) ); $article_price = floatval( wp_unslash( $_POST['acta_article_price'] ?? 0 ) ); if ( empty( $email ) || empty( $first_name ) || empty( $last_name ) || empty( $country ) || empty( $currency ) || $article_price <= 0 ) { echo '
' . esc_html__( 'All fields are required. Please fill in your email, name, country, currency, and a price greater than 0.', 'acta-pay-per-article' ) . '
' . esc_html__( 'Successfully registered! Complete your Stripe setup below to start accepting payments.', 'acta-pay-per-article' ) . '
' . esc_html( $result['message'] ) . '
' . esc_html__( 'Status updated. If your Stripe setup is complete, you\'re live!', 'acta-pay-per-article' ) . '
' . esc_html__( 'Default price updated to ', 'acta-pay-per-article' ) . esc_html( number_format( $result['articlePrice'], 2 ) ) . '.
' . esc_html( $result['message'] ) . '
' . ( '1' === $override ? esc_html__( 'Saved. Acta will now replace your subscribe paywall with a pay-per-article button.', 'acta-pay-per-article' ) : esc_html__( 'Saved. Acta will mount alongside your existing subscription paywall.', 'acta-pay-per-article' ) ) . '
' . esc_html( $result['message'] ) . '
' . esc_html__( 'Acta is disconnected. Your account and any balance owed are safe — reconnect any time.', 'acta-pay-per-article' ) . '
' . esc_html( $result['message'] ) . '
' . esc_html__( 'Reconnected. Acta is live on your site again.', 'acta-pay-per-article' ) . '
' . esc_html( $result['message'] ) . '
Acta is a pay-per-content solution for publishers and creators.
Version
Publisher ID:
You need to set up your Stripe account to receive payments from readers. This takes about 5 minutes.
✓ Stripe setup complete — readers can now purchase your articles.
Publisher ID:
Acta adds a “Read instantly” button to the paywall you already use, giving readers the option to buy a single article. Your subscription offer stays exactly as it is — Acta finds your paywall and adds the button automatically.
Works with: Jetpack, MemberPress, Paid Memberships Pro, Restrict Content Pro, and more. If something looks off, just contact us and we’ll sort it out.
No paywall yet? Install the free Jetpack plugin and add a Paywall block where the article should lock — Acta integrates right within it.
Your price: Set any price per article (see currency minimums).
Our fee: No setup or monthly fee; % rev share including processor fees.
Questions or need setup help? Reach us at contact@readwithacta.com.
Current default price:
By default, all articles use your default price. To set a different price on a specific article, add a Custom HTML block in the post editor (before the paywall break) with this snippet:
<div id="acta-price" data-price="ENTER_PRICE_HERE"></div>
Replace ENTER_PRICE_HERE with the price (e.g. 2.99). This overrides the default price for that article only.
Turns off the Acta button on your site. Your account, your Stripe details and any money owed to you are kept — you can reconnect any time without signing up again.
Prefer this to deleting the plugin: deleting removes the settings stored on your site, and readers who already bought an article lose access to it.
The Acta button is not being shown on your site and no new purchases are being taken.
Your account is intact — Publisher ID: . Any balance owed to you is unaffected.
Contact us at contact@readwithacta.com or visit readwithacta.com