diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2b6a80c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +.gitattributes export-ignore +.gitignore export-ignore +.github export-ignore +CONTRIBUTING.md export-ignore +README.md export-ignore +Dockerfile export-ignore +pmpro-paystack-banner.jpg export-ignore \ No newline at end of file diff --git a/.github/workflows/generate-translations.yml b/.github/workflows/generate-translations.yml new file mode 100644 index 0000000..4aa4ca4 --- /dev/null +++ b/.github/workflows/generate-translations.yml @@ -0,0 +1,18 @@ +name: Generate Translations +on: workflow_dispatch +jobs: + generate-translations: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: WordPress POT/PO/MO Generator + uses: strangerstudios/action-wp-pot-po-mo-generator@main + with: + generate_pot: 1 + generate_po: 1 + generate_mo: 1 + generate_lang_packs: 1 + merge_changes: 1 + headers: '{"Report-Msgid-Bugs-To":"info@paidmembershipspro.com","Last-Translator":"Paid Memberships Pro ","Language-Team":"Paid Memberships Pro "}' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml new file mode 100644 index 0000000..67cfbaf --- /dev/null +++ b/.github/workflows/sync-labels.yml @@ -0,0 +1,23 @@ +name: Sync labels +on: + # You can run this with every type of event, but it's better to run it only when you actually need it. + workflow_dispatch: + +jobs: + labels: + runs-on: ubuntu-latest + + steps: + - uses: strangerstudios/label-sync@v2 + with: + # If you want to use a source repo, you can put is name here (only the owner/repo format is accepted) + source-repo: strangerstudios/paid-memberships-pro + + # If you want to delete any additional label, set this to true + delete-other-labels: true + + #If you want the action just to show you the preview of the changes, without actually editing the labels, set this to tru + dry-run: false + + # You can change the token used to change the labels, this is the default one + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 3b77d14..0000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM ubuntu:18.04 AS builder - -WORKDIR /dist - -RUN apt-get -y install && apt-get update -RUN apt-get -y install wget unzip zip - -ADD https://downloads.wordpress.org/plugin/paid-memberships-pro.latest-stable.zip /dist -RUN cd /dist && unzip paid-memberships-pro.latest-stable.zip && rm paid-memberships-pro.latest-stable.zip - - -FROM wordpress:php7.2 - -WORKDIR /var/www/html/ - -COPY . ./wp-content/plugins/paystack -COPY --from=builder /dist/ ./wp-content/plugins diff --git a/README.md b/README.md index 177a98d..ac5ba37 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,37 @@ -

Paystack Paid Membership Pro

+![](pmpro-paystack-banner.jpg) -# Paystack Gateway for Paid Membership Pro +# [Paid Memberships Pro - Paystack Gateway](https://www.paidmembershipspro.com/add-ons/paystack-gateway/) # -Welcome to the Paystack Gateway for Paid Membership Pro repository on GitHub. +![WordPress Plugin Downloads](https://img.shields.io/wordpress/plugin/dy/paystack-gateway-paid-memberships-pro?style=flat-square) ![License](https://img.shields.io/badge/license-GPL--2.0%2B-red.svg?style=flat-square) -The *Paystack Gateway for Paid Membership Pro* plugin allows Wordpress site owners from Nigeria and Ghana to accept payments from their customers via Paid Membership Pro. +### Welcome to the Paid Memberships Pro - Paystack Gateway GitHub Repository +Add Paystack as a gateway option for Paid Memberships Pro and accept payments from members around Africa. -Here you can browse the source, look at open issues and keep track of development. +For more information please visit [paidmembershipspro.com/add-ons/paystack-gateway/](https://www.paidmembershipspro.com/add-ons/paystack-gateway/) -## Installation +## Installation ## +For detailed installation steps, visit the [documentation](https://www.paidmembershipspro.com/add-ons/paystack-gateway/) page. -1. Install the [Paystack Gateway for Paid Membership Pro](https://wordpress.org/plugins/paystack-gateway-paid-memberships-pro/) via the Plugins section of your WordPress Dashboard. -2. Go to the settings section of the plugin and enter your Public and Secret Keys which are available on your [Paystack Dashboard](https://dashboard.paystack.com/#/settings/developer). +1. Download the current development ZIP file directly: `https://github.com/strangerstudios/paystack-gateway-paid-memberships-pro/archive/dev.zip` -## Documentation -* [Paystack Documentation](https://developers.paystack.co/v2.0/docs/) -* [Paystack Helpdesk](https://paystack.com/help) +**Please ensure that once installing this version of the plugin to remove `-dev` from the plugin's folder name.** -## Support -For bug reports and feature requests directly related to this plugin, please use the [issue tracker](https://github.com/PaystackHQ/plugin-paid-membership-pro/issues). +## Bugs ## +If you find an issue/bug, let us know by [creating a detailed GitHub issue](https://github.com/strangerstudios/paystack-gateway-paid-memberships-pro/issues/new). -For questions related to using the plugin, please post an inquiry to the plugin [support forum](https://wordpress.org/support/plugin/paystack-gateway-paid-memberships-pro). +## Support ## +This is a developer's portal for Paid Memberships Pro - Paystack Gateway. We do not offer support on this channel. **Any support related questions should be directed to [paidmembershipspro.com/add-ons/paystack-gateway/](https://www.paidmembershipspro.com/add-ons/paystack-gateway/).** -For general support or questions about your Paystack account, you can reach out by sending a message from [our website](https://paystack.com/contact). +## Contributing to Paid Memberships Pro - Paystack Gateway ## +We encourage and welcome any contribution to Paid Memberships Pro - Paystack Gateway. Please read the [guidelines for contributing](https://github.com/strangerstudios/paid-memberships-pro/blob/dev/.github/CONTRIBUTING.md) to this repository. -## Community -If you are a developer, please join our Developer Community on [Slack](https://slack.paystack.com). +There are various **ways to the help development** of Paid Memberships Pro - Paystack Gateway: -## Contributing to Paystack Gateway for Paid Membership Pro -If you have a patch or have stumbled upon an issue with the Paystack Gateway for Paid Membership Pro plugin, you can contribute this back to the code. Please read our [contributor guidelines](https://github.com/PaystackHQ/wordpress-paid-membership-pro-paystack/blob/master/CONTRIBUTING.md) for more information how you can do this. +1. Report [bugs/issues](https://github.com/strangerstudios/paystack-gateway-paid-memberships-pro/issues/new) on GitHub. +2. Work on any issues by submitting a Pull Request. + +Here are some ways for **non-developers to contribute** to Paid Memberships Pro - Paystack Gateway: + +1. Translate Paid Memberships Pro - Paystack Gateway into your own [language](https://www.paidmembershipspro.com/paid-memberships-pro-in-your-language/). +2. [Purchase a paid membership](https://paidmembershipspro.com/pricing) to help fund ongoing development and bug fixes. +3. Leave an honest review for [Paid Memberships Pro - Paystack Gateway](https://www.paidmembershipspro.com/submit-testimonial/). \ No newline at end of file diff --git a/class.pmprogateway_paystack.php b/class.pmprogateway_paystack.php deleted file mode 100755 index 12630ef..0000000 --- a/class.pmprogateway_paystack.php +++ /dev/null @@ -1,976 +0,0 @@ -gateway = $gateway; - $this->gateway_environment = pmpro_getOption("gateway_environment"); - - return $this->gateway; - } - - /** - * Run on WP init - */ - static function init() - { - //make sure Paystack is a gateway option - add_filter('pmpro_gateways', array('PMProGateway_Paystack', 'pmpro_gateways')); - - //add fields to payment settings - add_filter('pmpro_payment_options', array('PMProGateway_Paystack', 'pmpro_payment_options')); - add_filter('pmpro_payment_option_fields', array('PMProGateway_Paystack', 'pmpro_payment_option_fields'), 10, 2); - add_action('wp_ajax_kkd_pmpro_paystack_ipn', array('PMProGateway_Paystack', 'kkd_pmpro_paystack_ipn')); - add_action('wp_ajax_nopriv_kkd_pmpro_paystack_ipn', array('PMProGateway_Paystack', 'kkd_pmpro_paystack_ipn')); - //code to add at checkout - $gateway = pmpro_getGateway(); - if ($gateway == "paystack") { - add_filter('pmpro_include_billing_address_fields', '__return_false'); - add_filter('pmpro_required_billing_fields', array('PMProGateway_Paystack', 'pmpro_required_billing_fields')); - add_filter('pmpro_include_payment_information_fields', '__return_false'); - add_filter('pmpro_checkout_before_change_membership_level', array('PMProGateway_Paystack', 'pmpro_checkout_before_change_membership_level'), 10, 2); - - add_filter('pmpro_gateways_with_pending_status', array('PMProGateway_Paystack', 'pmpro_gateways_with_pending_status')); - add_filter('pmpro_pages_shortcode_checkout', array('PMProGateway_Paystack', 'pmpro_pages_shortcode_checkout'), 20, 1); - add_filter('pmpro_checkout_default_submit_button', array('PMProGateway_Paystack', 'pmpro_checkout_default_submit_button')); - // custom confirmation page - add_filter('pmpro_pages_shortcode_confirmation', array('PMProGateway_Paystack', 'pmpro_pages_shortcode_confirmation'), 20, 1); - } - } - - /** - * Redirect Settings to PMPro settings - */ - static function plugin_action_links($links, $file) - { - static $this_plugin; - - if (false === isset($this_plugin) || true === empty($this_plugin)) { - $this_plugin = plugin_basename(__FILE__); - } - - if ($file == $this_plugin) { - $settings_link = ''.__('Settings', KKD_PAYSTACKPMP).''; - array_unshift($links, $settings_link); - } - - return $links; - } - static function pmpro_checkout_default_submit_button($show) - { - global $gateway, $pmpro_requirebilling; - - //show our submit buttons - ?> - - - - - __('Paystack', KKD_PAYSTACKPMP)) + array_slice($gateways, 1); - } - return $gateways; - } - function kkd_pmprosd_convert_date( $date ) { - // handle lower-cased y/m values. - $set_date = strtoupper($date); - - // Change "M-" and "Y-" to "M1-" and "Y1-". - $set_date = preg_replace('/Y-/', 'Y1-', $set_date); - $set_date = preg_replace('/M-/', 'M1-', $set_date); - - // Get number of months and years to add. - $m_pos = stripos( $set_date, 'M' ); - $y_pos = stripos( $set_date, 'Y' ); - if($m_pos !== false) { - $add_months = intval( pmpro_getMatches( '/M([0-9]*)/', $set_date, true ) ); - } - if($y_pos !== false) { - $add_years = intval( pmpro_getMatches( '/Y([0-9]*)/', $set_date, true ) ); - } - - // Allow new dates to be set from a custom date. - if(empty($current_date)) $current_date = current_time( 'timestamp' ); - - // Get current date parts. - $current_y = intval(date('Y', $current_date)); - $current_m = intval(date('m', $current_date)); - $current_d = intval(date('d', $current_date)); - - // Get set date parts. - $date_parts = explode( '-', $set_date); - $set_y = intval($date_parts[0]); - $set_m = intval($date_parts[1]); - $set_d = intval($date_parts[2]); - - // Get temporary date parts. - $temp_y = $set_y > 0 ? $set_y : $current_y; - $temp_m = $set_m > 0 ? $set_m : $current_m; - $temp_d = $set_d; - - // Add months. - if(!empty($add_months)) { - for($i = 0; $i < $add_months; $i++) { - // If "M1", only add months if current date of month has already passed. - if(0 == $i) { - if($temp_d < $current_d) { - $temp_m++; - $add_months--; - } - } else { - $temp_m++; - } - - // If we hit 13, reset to Jan of next year and subtract one of the years to add. - if($temp_m == 13) { - $temp_m = 1; - $temp_y++; - $add_years--; - } - } - } - - // Add years. - if(!empty($add_years)) { - for($i = 0; $i < $add_years; $i++) { - // If "Y1", only add years if current date has already passed. - if(0 == $i) { - $temp_date = strtotime(date("{$temp_y}-{$temp_m}-{$temp_d}")); - if($temp_date < $current_date) { - $temp_y++; - $add_years--; - } - } else { - $temp_y++; - } - } - } - - // Pad dates if necessary. - $temp_m = str_pad($temp_m, 2, '0', STR_PAD_LEFT); - $temp_d = str_pad($temp_d, 2, '0', STR_PAD_LEFT); - - // Put it all together. - $set_date = date("{$temp_y}-{$temp_m}-{$temp_d}"); - - // Make sure we use the right day of the month for dates > 28 - // From: http://stackoverflow.com/a/654378/1154321 - $dotm = pmpro_getMatches('/\-([0-3][0-9]$)/', $set_date, true); - if ( $temp_m == '02' && intval($dotm) > 28 || intval($dotm) > 30 ) { - $set_date = date('Y-m-t', strtotime(substr($set_date, 0, 8) . "01")); - } - - - - return $set_date; - } - function kkd_pmpro_paystack_ipn() - { - global $wpdb; - // if ((strtoupper($_SERVER['REQUEST_METHOD']) != 'POST' ) || !array_key_exists('HTTP_X_PAYSTACK_SIGNATURE', $_SERVER) ) { - // exit(); - // } - define('SHORTINIT', true); - $input = @file_get_contents("php://input"); - $event = json_decode($input); - // echo "
";
-                    // print_r($event);
-                    // if(!$_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] || ($_SERVER['HTTP_X_PAYSTACK_SIGNATURE'] !== hash_hmac('sha512', $input, paystack_recurrent_billing_get_secret_key()))){
-                    //   exit();
-                    // }
-                    switch($event->event){
-                    case 'subscription.create':
-
-                        break;
-                    case 'subscription.disable':
-                        $amount = $event->data->subscription->amount/100;
-                        $morder = new MemberOrder();
-                        $subscription_code = $event->data->subscription_code;
-                        $email = $event->data->customer->email;
-                        $morder->Email = $email;
-                        $users_row = $wpdb->get_row( "SELECT ID, display_name FROM $wpdb->users WHERE user_email = '" . $email. "' LIMIT 1" );
-                        if ( ! empty( $users_row )  ) {
-                            $user_id = $users_row->ID;
-                            $user = get_userdata($user_id);
-                            $user->membership_level = pmpro_getMembershipLevelForUser($user_id);
-                        }
-                        if (empty($user)) {
-                            print_r('Could not get user');
-                            exit();
-                        }
-                        self::cancelMembership($user);
-                        break;
-                    case 'charge.success':
-                        $morder =  new MemberOrder($event->data->reference);
-                        $morder->getMembershipLevel();
-                        $morder->getUser();
-                        $morder->Gateway->pmpro_pages_shortcode_confirmation('', $event->data->reference);
-                        $mode = pmpro_getOption("gateway_environment");
-                        if ($mode == 'sandbox') {
-                            $pk = pmpro_getOption("paystack_tpk");
-                        } else {
-                            $pk = pmpro_getOption("paystack_lpk");
-                        }
-                        $pstk_logger = new pmpro_paystack_plugin_tracker('pm-pro',$pk);
-                        $pstk_logger->log_transaction_success($event->data->reference);
-                        break;
-                    case 'invoice.create':
-                        self::renewpayment($event);
-                    case 'invoice.update':
-                        self::renewpayment($event);
-                  
-                    }
-                    http_response_code(200);
-                    exit();
-                }
-
-                /**
-                 * Get a list of payment options that the Paystack gateway needs/supports.
-                 */
-                static function getGatewayOptions()
-                {
-                    $options = array (
-                        'paystack_tsk',
-                        'paystack_tpk',
-                        'paystack_lsk',
-                        'paystack_lpk',
-                        'gateway_environment',
-                        'currency',
-                        'tax_state',
-                        'tax_rate'
-                        );
-
-                    return $options;
-                }
-
-                /**
-                 * Set payment options for payment settings page.
-                 */
-                static function pmpro_payment_options($options)
-                {
-                    //get Paystack options
-                    $paystack_options = self::getGatewayOptions();
-
-                    //merge with others.
-                    $options = array_merge($paystack_options, $options);
-
-                    return $options;
-                }
-
-                /**
-                 * Display fields for Paystack options.
-                 */
-                static function pmpro_payment_option_fields($values, $gateway)
-                {
-                    ?>
-                    style="display: none;">
-                        
-                            
-                        
-                    
-                    style="display: none;">
-                        
-                            
-                        
-                        
-                            

- - - - style="display: none;"> - - - - - - - - style="display: none;"> - - - - - - - - style="display: none;"> - - - - - - - - style="display: none;"> - - - - - - - - - - getLastMemberOrder(get_current_user_id(), apply_filters("pmpro_confirmation_order_status", array("pending"))); - - if ((!in_array("paystack", $gateways)) && $found) { - array_push($gateways, "paystack"); - } elseif (($key = array_search("paystack", $gateways)) !== false) { - unset($gateways[$key]); - } - - return $gateways; - } - - /** - * Instead of change membership levels, send users to Paystack payment page. - */ - static function pmpro_checkout_before_change_membership_level($user_id, $morder) - { - global $wpdb, $discount_code_id; - - //if no order, no need to pay - if (empty($morder)) { - return; - } - if (empty($morder->code)) - $morder->code = $morder->getRandomCode(); - - $morder->payment_type = "paystack"; - $morder->status = "pending"; - $morder->user_id = $user_id; - $morder->saveOrder(); - - //save discount code use - if (!empty($discount_code_id)) - $wpdb->query("INSERT INTO $wpdb->pmpro_discount_codes_uses (code_id, user_id, order_id, timestamp) VALUES('" . $discount_code_id . "', '" . $user_id . "', '" . $morder->id . "', now())"); - - $morder->Gateway->sendToPaystack($morder); - } - - function sendToPaystack(&$order) - { - global $wp, $pmpro_currency; - - do_action("pmpro_paypalexpress_session_vars"); - - $params = array(); - $amount = $order->PaymentAmount; - $amount_tax = $order->getTaxForPrice($amount); - $amount = round((float)$amount + (float)$amount_tax, 2); - - //call directkit to get Webkit Token - $amount = floatval($order->InitialPayment); - - // echo pmpro_url("confirmation", "?level=" . $order->membership_level->id); - // die(); - $mode = pmpro_getOption("gateway_environment"); - if ($mode == 'sandbox') { - $key = pmpro_getOption("paystack_tsk"); - $pk = pmpro_getOption("paystack_tpk"); - } else { - $key = pmpro_getOption("paystack_lsk"); - $pk = pmpro_getOption("paystack_lpk"); - } - if ($key == '') { - echo "Api keys not set"; - } - // $txn_code = $txn.'_'.$order_id; - - $koboamount = $amount*100; - // $mcurrency = - - // foreach($level_currencies as $level_currency_id => $level_currency) - // { - // if($level_id == $level_currency_id) - // { - // $pmpro_currency = $level_currency[0]; - // $pmpro_currency_symbol = $level_currency[1]; - - // } - // } - - $paystack_url = 'https://api.paystack.co/transaction/initialize'; - $headers = array( - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer '.$key - ); - - //Create Plan - $body = array( - 'email' => $order->Email, - 'amount' => $koboamount, - 'reference' => $order->code, - 'currency' => $pmpro_currency, - 'callback_url' => pmpro_url("confirmation", "?level=" . $order->membership_level->id), - 'metadata' => json_encode(array('custom_fields' => array( - array( - "display_name"=>"Plugin", - "variable_name"=>"plugin", - "value"=>"pm-pro" - ), - - ), 'custom_filters' => array("recurring" => true))), - - ); - $args = array( - 'body' => json_encode($body), - 'headers' => $headers, - 'timeout' => 60 - ); - - $request = wp_remote_post($paystack_url, $args); - // print_r($request); - if (!is_wp_error($request)) { - $paystack_response = json_decode(wp_remote_retrieve_body($request)); - if ($paystack_response->status){ - $url = $paystack_response->data->authorization_url; - wp_redirect($url); - exit; - } else { - $order->Gateway->delete($order); - wp_redirect(pmpro_url("checkout", "?level=" . $order->membership_level->id . "&error=" . $paystack_response->message)); - exit(); - } - } else { - $order->Gateway->delete($order); - wp_redirect(pmpro_url("checkout", "?level=" . $order->membership_level->id . "&error=Failed")); - exit(); - } - exit; - } - static function renewpayment($event) - { - global $wp,$wpdb; - - if (isset($event->data->paid) && ($event->data->paid == 1)) { - - $amount = $event->data->subscription->amount/100; - $old_order = new MemberOrder(); - $subscription_code = $event->data->subscription->subscription_code; - $email = $event->data->customer->email; - $old_order->getLastMemberOrderBySubscriptionTransactionID($subscription_code); - - if (empty($old_order)) { - exit(); - } - $user_id = $old_order->user_id; - $user = get_userdata($user_id); - $user->membership_level = pmpro_getMembershipLevelForUser($user_id); - - if (empty($user)) { - exit(); - } - - $morder = new MemberOrder(); - $morder->user_id = $old_order->user_id; - $morder->membership_id = $old_order->membership_id; - $morder->InitialPayment = $amount; //not the initial payment, but the order class is expecting this - $morder->PaymentAmount = $amount; - $morder->payment_transaction_id = $event->data->invoice_code; - $morder->subscription_transaction_id = $subscription_code; - - $morder->gateway = $old_order->gateway; - $morder->gateway_environment = $old_order->gateway_environment; - - $morder->Email = $email; - $pmpro_level = $wpdb->get_row("SELECT * FROM $wpdb->pmpro_membership_levels WHERE id = '" . (int)$morder->membership_id . "' LIMIT 1"); - $pmpro_level = apply_filters("pmpro_checkout_level", $pmpro_level); - $startdate = apply_filters("pmpro_checkout_start_date", "'" . current_time("mysql") . "'", $morder->user_id, $pmpro_level); - - $enddate = "'" . date("Y-m-d", strtotime("+ " . $pmpro_level->expiration_number . " " . $pmpro_level->expiration_period, current_time("timestamp"))) . "'"; - - $custom_level = array( - 'user_id' => $morder->user_id, - 'membership_id' => $pmpro_level->id, - 'code_id' => '', - 'initial_payment' => $pmpro_level->initial_payment, - 'billing_amount' => $pmpro_level->billing_amount, - 'cycle_number' => $pmpro_level->cycle_number, - 'cycle_period' => $pmpro_level->cycle_period, - 'billing_limit' => $pmpro_level->billing_limit, - 'trial_amount' => $pmpro_level->trial_amount, - 'trial_limit' => $pmpro_level->trial_limit, - 'startdate' => $startdate, - 'enddate' => $enddate - ); - - //get CC info that is on file - $morder->expirationmonth = get_user_meta($user_id, "pmpro_ExpirationMonth", true); - $morder->expirationyear = get_user_meta($user_id, "pmpro_ExpirationYear", true); - $morder->ExpirationDate = $morder->expirationmonth . $morder->expirationyear; - $morder->ExpirationDate_YdashM = $morder->expirationyear . "-" . $morder->expirationmonth; - - - //save - if ($morder->status != 'success') { - - $_REQUEST['cancel_membership'] = false; // Do NOT cancel gateway subscription - - if (pmpro_changeMembershipLevel($custom_level, $morder->user_id, 'changed')) { - $morder->status = "success"; - $morder->saveOrder(); - } - - } - $morder->getMemberOrderByID($morder->id); - - //email the user their invoice - $pmproemail = new PMProEmail(); - $pmproemail->sendInvoiceEmail($user, $morder); - - do_action('pmpro_subscription_payment_completed', $morder); - exit(); - } - - } - - static function pmpro_pages_shortcode_checkout($content) - { - $morder = new MemberOrder(); - $found = $morder->getLastMemberOrder(get_current_user_id(), apply_filters("pmpro_confirmation_order_status", array("pending"))); - if ($found) { - $morder->Gateway->delete($morder); - } - - if (isset($_REQUEST['error'])) { - global $pmpro_msg, $pmpro_msgt; - - $pmpro_msg = __("IMPORTANT: Something went wrong during the payment. Please try again later or contact the site owner to fix this issue.
" . urldecode($_REQUEST['error']), "pmpro"); - $pmpro_msgt = "pmpro_error"; - - $content = "
" . $pmpro_msg . "
" . $content; - } - - return $content; - } - - /** - * Custom confirmation page - */ - static function pmpro_pages_shortcode_confirmation($content,$reference = null) - { - global $wpdb, $current_user, $pmpro_invoice, $pmpro_currency,$gateway; - if (!isset($_REQUEST['trxref'])) { - $_REQUEST['trxref'] = null; - } - if ($reference != null) { - $_REQUEST['trxref'] = $reference; - } - - - if (empty($pmpro_invoice)) { - $morder = new MemberOrder($_REQUEST['trxref']); - // $morder = new MemberOrder(); - // $morder->getLastMemberOrder(get_current_user_id(), apply_filters("pmpro_confirmation_order_status", array("pending", "success"))); - if (!empty($morder) && $morder->gateway == "paystack") $pmpro_invoice = $morder; - } - - if (!empty($pmpro_invoice) && $pmpro_invoice->gateway == "paystack" && isset($pmpro_invoice->total) && $pmpro_invoice->total > 0) { - $morder = $pmpro_invoice; - if ($morder->code == $_REQUEST['trxref']) { - $pmpro_level = $wpdb->get_row("SELECT * FROM $wpdb->pmpro_membership_levels WHERE id = '" . (int)$morder->membership_id . "' LIMIT 1"); - $pmpro_level = apply_filters("pmpro_checkout_level", $pmpro_level); - $startdate = apply_filters("pmpro_checkout_start_date", "'" . current_time("mysql") . "'", $morder->user_id, $pmpro_level); - - $mode = pmpro_getOption("gateway_environment"); - if ($mode == "sandbox") { - $key = pmpro_getOption("paystack_tsk"); - $pk = pmpro_getOption("paystack_tpk"); - } else { - $key = pmpro_getOption("paystack_lsk"); - $pk = pmpro_getOption("paystack_lpk"); - } - $paystack_url = 'https://api.paystack.co/transaction/verify/' . $_REQUEST['trxref']; - $headers = array( - 'Authorization' => 'Bearer ' . $key - ); - $args = array( - 'headers' => $headers, - 'timeout' => 60 - ); - $request = wp_remote_get($paystack_url, $args); - if (!is_wp_error($request) && 200 == wp_remote_retrieve_response_code($request) ) { - $paystack_response = json_decode(wp_remote_retrieve_body($request)); - // if ('success' == $paystack_response->data->status && $pmpro_level->initial_payment == ($paystack_response->data->amount/100)) { - if ('success' == $paystack_response->data->status && $morder->total == ($paystack_response->data->amount / 100)) { - $customer_code = $paystack_response->data->customer->customer_code; - - //Add logger here - $pstk_logger = new pmpro_paystack_plugin_tracker('pm-pro',$pk); - $pstk_logger->log_transaction_success($_REQUEST['trxref']); - do_action('pmpro_after_checkout', $morder->user_id, $morder); - //-------------------------------------------------- - - if (strlen($morder->subscription_transaction_id) > 3) { - $enddate = "'" . date("Y-m-d", strtotime("+ " . $morder->subscription_transaction_id, current_time("timestamp"))) . "'"; - - // Override the previous calculation - $__date = date_create(date("Y-m-d H:i:s", current_time("timestamp"))); - date_add($__date, date_interval_create_from_date_string("".$pmpro_level->cycle_number." ".$pmpro_level->cycle_period."")); - - $enddate = "'" . $__date->format("Y-m-d H:i:s") . "'"; - } elseif (!empty($pmpro_level->expiration_number)) { - $enddate = "'" . date("Y-m-d", strtotime("+ " . $pmpro_level->expiration_number . " " . $pmpro_level->expiration_period, current_time("timestamp"))) . "'"; - } else { - $enddate = "NULL"; - } - if (($pmpro_level->cycle_number > 0) && ($pmpro_level->billing_amount > 0) && ($pmpro_level->cycle_period != "")) { - if ($pmpro_level->cycle_number < 10 && $pmpro_level->cycle_period == 'Day') { - $interval = 'weekly'; - } elseif (($pmpro_level->cycle_number == 90) && ($pmpro_level->cycle_period == 'Day')) { - $interval = 'quarterly'; - } - elseif (($pmpro_level->cycle_number == 180) && ($pmpro_level->cycle_period == 'Day')) { - $interval = 'biannually'; - } - elseif (($pmpro_level->cycle_number >= 10) && ($pmpro_level->cycle_period == 'Day')) { - $interval = 'monthly'; - } elseif (($pmpro_level->cycle_number == 3) && ($pmpro_level->cycle_period == 'Month')) { - $interval = 'quarterly'; - - } - elseif (($pmpro_level->cycle_number == 6) && ($pmpro_level->cycle_period == 'Month')) { - $interval = 'biannually'; - - } - elseif (($pmpro_level->cycle_number > 0) && ($pmpro_level->cycle_period == 'Month')) { - $interval = 'monthly'; - } elseif (($pmpro_level->cycle_number > 0) && ($pmpro_level->cycle_period == 'Year')) { - $interval = 'annually'; - } - - $amount = $pmpro_level->billing_amount; - $koboamount = $amount*100; - //Create Plan - $paystack_url = 'https://api.paystack.co/plan'; - $subscription_url = 'https://api.paystack.co/subscription'; - $check_url = 'https://api.paystack.co/plan?amount='.$koboamount.'&interval='.$interval; - $headers = array( - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer ' . $key - ); - - $checkargs = array( - 'headers' => $headers, - 'timeout' => 60 - ); - // Check if plan exist - $checkrequest = wp_remote_get($check_url, $checkargs); - if (!is_wp_error($checkrequest)) { - $response = json_decode(wp_remote_retrieve_body($checkrequest)); - if ($response->meta->total >= 1) { - $plan = $response->data[0]; - $plancode = $plan->plan_code; - - } else { - //Create Plan - $body = array( - 'name' => '('.number_format($amount).') - '.$interval.' - ['.$pmpro_level->cycle_number.' - '.$pmpro_level->cycle_period.']' , - 'amount' => $koboamount, - 'interval' => $interval - ); - $args = array( - 'body' => json_encode($body), - 'headers' => $headers, - 'timeout' => 60 - ); - - $request = wp_remote_post($paystack_url, $args); - if (!is_wp_error($request)) { - $paystack_response = json_decode(wp_remote_retrieve_body($request)); - $plancode = $paystack_response->data->plan_code; - } - } - - } - $subscription_delay = get_option( 'pmpro_subscription_delay_' . $pmpro_level->id, 0 ); - if($subscription_delay) - if ( ! is_numeric( $subscription_delay ) ) { - $start_date = kkd_pmprosd_convert_date( $subscription_delay ); - } else { - $start_date = date( 'Y-m-d', strtotime( '+ ' . intval( $subscription_delay ) . ' Days', current_time( 'timestamp' ) ) ); - } - - $body = array( - 'customer' => $customer_code, - 'plan' => $plancode, - 'start_date' => $start_date - ); - $args = array( - 'body' => json_encode($body), - 'headers' => $headers, - 'timeout' => 60 - ); - - $request = wp_remote_post($subscription_url, $args); - if (!is_wp_error($request)) { - $paystack_response = json_decode(wp_remote_retrieve_body($request)); - $subscription_code = $paystack_response->data->subscription_code; - $token = $paystack_response->data->email_token; - $morder->subscription_transaction_id = $subscription_code; - $morder->subscription_token = $token; - - - - } - - } - // - // die(); - - $custom_level = array( - 'user_id' => $morder->user_id, - 'membership_id' => $pmpro_level->id, - 'code_id' => '', - 'initial_payment' => $pmpro_level->initial_payment, - 'billing_amount' => $pmpro_level->billing_amount, - 'cycle_number' => $pmpro_level->cycle_number, - 'cycle_period' => $pmpro_level->cycle_period, - 'billing_limit' => $pmpro_level->billing_limit, - 'trial_amount' => $pmpro_level->trial_amount, - 'trial_limit' => $pmpro_level->trial_limit, - 'startdate' => $startdate, - 'enddate' => $enddate - ); - if ($morder->status != 'success') { - - $_REQUEST['cancel_membership'] = false; // Do NOT cancel gateway subscription - - if (pmpro_changeMembershipLevel($custom_level, $morder->user_id, 'changed')) { - $morder->membership_id = $pmpro_level->id; - $morder->payment_transaction_id = $_REQUEST['trxref']; - $morder->status = "success"; - $morder->saveOrder(); - } - - } - // echo "
";
-                                    // print_r($morder);
-                                    // die();
-                                    //setup some values for the emails
-                                    if (!empty($morder)) {
-                                        $pmpro_invoice = new MemberOrder($morder->id);
-                                    } else {
-                                        $pmpro_invoice = null;
-                                    }
-
-                                    $current_user->membership_level = $pmpro_level; //make sure they have the right level info
-                                    $current_user->membership_level->enddate = $enddate;
-                                    if ($current_user->ID) {
-                                        $current_user->membership_level = pmpro_getMembershipLevelForUser($current_user->ID);
-                                        // echo "interesting";
-                                    }
-
-                                    //send email to member
-                                    $pmproemail = new PMProEmail();
-                                    $pmproemail->sendCheckoutEmail($current_user, $pmpro_invoice);
-
-                                    //send email to admin
-                                    $pmproemail = new PMProEmail();
-                                    $pmproemail->sendCheckoutAdminEmail($current_user, $pmpro_invoice);
-                                    // echo "
";
-                                    // print_r($pmpro_level);
-                                    $content = "
    -
  • ".__('Account', KKD_PAYSTACKPMP).": ".$current_user->display_name." (".$current_user->user_email.")
  • -
  • ".__('Order', KKD_PAYSTACKPMP).": ".$pmpro_invoice->code."
  • -
  • ".__('Membership Level', KKD_PAYSTACKPMP).": ".$pmpro_level->name."
  • -
  • ".__('Amount Paid', KKD_PAYSTACKPMP).": ".$pmpro_invoice->total." ".$pmpro_currency."
  • -
"; - ob_start(); - if (file_exists(get_stylesheet_directory() . "/paid-memberships-pro/pages/confirmation.php")) { - include get_stylesheet_directory() . "/paid-memberships-pro/pages/confirmation.php"; - } else { - include PMPRO_DIR . "/pages/confirmation.php"; - } - - $content .= ob_get_contents(); - ob_end_clean(); - } else { - $content = 'Invalid Reference'; - - } - - } else { - $content = 'Unable to Verify Transaction'; - - } - - } else { - $content = 'Invalid Transaction Reference'; - } - } - - - return $content; - - } - - function cancelMembership(&$user){ -// - if (empty($user)) { - print_r("Empty user object"); - exit(); - } - $user_id = $user->ID; - $level_to_cancel = $user->membership_level->ID; - if(empty($user_id) || empty($level_to_cancel)){ - exit(); - } - global $wpdb; - $memberships_users_row = $wpdb->get_row( "SELECT * FROM $wpdb->pmpro_memberships_users WHERE user_id = '" . $user_id. "' AND membership_id = '" . $level_to_cancel . "' AND status = 'active' LIMIT 1" ); - if ( ! empty( $memberships_users_row ) ) { - - $days_grace = 0; - $new_enddate = date( 'Y-m-d H:i:s', current_time( 'timestamp' ) + 3600 * 24 * $days_grace ); - $result = $wpdb->update( $wpdb->pmpro_memberships_users, array( 'enddate' => $new_enddate ), array( - 'user_id' => $user_id, - 'membership_id' => $level_to_cancel, - 'status' => 'active' - ), array( '%s' ), array( '%d', '%d', '%s' ) ); - print_r($result); - }else{ - print_r("No records were found with user - ". $user_id." level - ". $level_to_cancel); - } - } - function cancel(&$order) - { - - //no matter what happens below, we're going to cancel the order in our system - $order->updateStatus("cancelled"); - $mode = pmpro_getOption("gateway_environment"); - $code = $order->subscription_transaction_id; - if ($mode == 'sandbox') { - $key = pmpro_getOption("paystack_tsk"); - } else { - $key = pmpro_getOption("paystack_lsk"); - - } - if ($order->subscription_transaction_id != "") { - $paystack_url = 'https://api.paystack.co/subscription/' . $code; - $headers = array( - 'Authorization' => 'Bearer ' . $key - ); - $args = array( - 'headers' => $headers, - 'timeout' => 60 - ); - $request = wp_remote_get($paystack_url, $args); - if (!is_wp_error($request) && 200 == wp_remote_retrieve_response_code($request)) { - $paystack_response = json_decode(wp_remote_retrieve_body($request)); - if ('active' == $paystack_response->data->status && $code == $paystack_response->data->subscription_code && '1' == $paystack_response->status) { - - $paystack_url = 'https://api.paystack.co/subscription/disable'; - $headers = array( - 'Content-Type' => 'application/json', - 'Authorization' => "Bearer ".$key - ); - $body = array( - 'code' => $paystack_response->data->subscription_code, - 'token' => $paystack_response->data->email_token, - - ); - $args = array( - 'body' => json_encode($body), - 'headers' => $headers, - 'timeout' => 60 - ); - - $request = wp_remote_post($paystack_url, $args); - // print_r($request); - if (!is_wp_error($request)) { - $paystack_response = json_decode(wp_remote_retrieve_body($request)); - } - } - } - } - global $wpdb; - $wpdb->query("DELETE FROM $wpdb->pmpro_membership_orders WHERE id = '" . $order->id . "'"); - } - function delete(&$order) - { - //no matter what happens below, we're going to cancel the order in our system - $order->updateStatus("cancelled"); - global $wpdb; - $wpdb->query("DELETE FROM $wpdb->pmpro_membership_orders WHERE id = '" . $order->id . "'"); - } - } - } - } -} -?> diff --git a/classes/class.pmprogateway_paystack.php b/classes/class.pmprogateway_paystack.php new file mode 100644 index 0000000..a4aad9b --- /dev/null +++ b/classes/class.pmprogateway_paystack.php @@ -0,0 +1,832 @@ +gateway = $gateway; + return $this->gateway; + } + + /** + * Run on WP init. + * This method will run all the necessary gateway hooks that are needed. + */ + public static function init() { + + // Make sure Paystack is a gateway option. + add_filter( 'pmpro_gateways', array( 'PMProGateway_paystack', 'pmpro_gateways' ) ); + + // Add fields to payment settings. + add_filter( 'pmpro_payment_options', array( 'PMProGateway_Paystack', 'pmpro_payment_options' ) ); + add_filter( 'pmpro_payment_option_fields', array( 'PMProGateway_Paystack', 'pmpro_payment_option_fields' ), 10, 2 ); + add_action( 'wp_ajax_pmpro_paystack_ipn', array( 'PMProGateway_Paystack', 'pmpro_paystack_ipn' ) ); + add_action( 'wp_ajax_nopriv_pmpro_paystack_ipn', array( 'PMProGateway_Paystack', 'pmpro_paystack_ipn' ) ); + + // Keeping the deprecated action for backwards compatibility. + add_action( 'wp_ajax_kkd_pmpro_paystack_ipn', array( 'PMProGateway_Paystack', 'kkd_pmpro_paystack_ipn' ) ); + add_action( 'wp_ajax_nopriv_kkd_pmpro_paystack_ipn', array( 'PMProGateway_Paystack', 'kkd_pmpro_paystack_ipn' ) ); + + // Adjust the confirmation message when waiting for Paystack to process the payment. + add_filter( 'pmpro_confirmation_payment_incomplete_message', array( 'PMProGateway_Paystack', 'pmpro_confirmation_incomplete_message' ), 10, 2 ); + + $gateway = pmpro_getGateway(); + if ( $gateway == 'paystack' ) { + global $pmpro_gateway_ready; + $pmpro_gateway_ready = true; + + add_filter( 'pmpro_include_billing_address_fields', '__return_false' ); + add_filter( 'pmpro_include_payment_information_fields', '__return_false' ); + add_filter( 'pmpro_billing_show_payment_method', '__return_false' ); + add_filter( 'pmpro_required_billing_fields', array( 'PMProGateway_Paystack', 'pmpro_required_billing_fields' ) ); + + // Refund functionality. + add_filter( 'pmpro_allowed_refunds_gateways', array( 'PMProGateway_Paystack', 'pmpro_allowed_refunds_gateways' ) ); + add_filter( 'pmpro_process_refund_paystack', array( 'PMProGateway_Paystack', 'process_refund' ), 10, 2 ); + } + + } + + /** + * Add Paystack to the gateway list for PMPro + */ + public static function pmpro_gateways( $gateways ) { + if ( empty( $gateways['paystack'] ) ) { + $gateways['paystack'] = __( 'Paystack', 'text-domain' ); + } + + return $gateways; + } + + /** + * Wrapper function for newly named function instead to be more inline with PMPro naming conventions. + * DEPRECATED use pmpro_paystack_ipn instead. + * @since 1.0 + */ + static function kkd_pmpro_paystack_ipn() { + pmpro_paystack_ipn(); + } + + /** + * Webhook handler for Paystack. + * @since 1.0 (Renamed in 1.7.1) + */ + static function pmpro_paystack_ipn() { + require_once PMPRO_PAYSTACK_DIR . 'services/paystack-webhook.php'; + exit; + } + + /** + * Check whether or not a gateway supports a specific feature. + * + * @param string $feature The feature we need to check if it is supported. + * @return string|boolean $supports In some cases, we may need to return strings for the feature or a boolean value if it's supported or not. + */ + public static function supports( $feature ) { + $supports = array( + 'subscription_sync' => true, + 'payment_method_updates' => false + ); + + if ( empty( $supports[$feature] ) ) { + return false; + } + + return $supports[$feature]; + } + + /** + * Get a list of payment options that the Paystack gateway needs/supports. + */ + static function getGatewayOptions() { + $options = array ( + 'paystack_tsk', + 'paystack_tpk', + 'paystack_lsk', + 'paystack_lpk', + 'currency', + 'tax_state', + 'tax_rate' + ); + + return $options; + } + + /** + * Set payment options for payment settings page. + * This has been deprecated due to Paid Memberships Pro V3.5+. + */ + static function pmpro_payment_options( $options ) { + //get Paystack options + $paystack_options = self::getGatewayOptions(); + + //merge with others. + $options = array_merge( $paystack_options, $options ); + + return $options; + } + + /** + * Display fields for Paystack options. + * This function is deprecated and only used for versions before PMPro 3.5 + * See the new callback `show_settings_fields` in the `PMProGateway_Paystack` class. + */ + static function pmpro_payment_option_fields( $values, $gateway ) { + ?> + style="display: none;"> + + + + + + + + style="display: none;"> + + + + + + + + style="display: none;"> + + + + + + + + style="display: none;"> + + + + + + + + style="display: none;"> + + + + +


+ + + + +

+ ' . esc_html__( 'Paystack documentation', 'paystack-gateway-paid-memberships-pro' ) . '' + ); + ?> +

+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + +


+
+
+
+ code ) ) { + $order->code = $order->getRandomCode(); + } + + // clean up some values and save the token order so we can complete it later. + $order->payment_type = 'Paystack'; + $order->CardType = ''; + $order->cardtype = ''; + $order->status = 'token'; + $order->saveOrder(); + + pmpro_save_checkout_data_to_order( $order ); + + do_action( 'pmpro_before_send_to_paystack', $order->user_id, $order ); + + $this->sendToPaystack( $order ); + } + + /** + * Redirect to Paystack to charge the payment. + */ + + /// Todo: Refactor using newew order code methods. + function sendToPaystack( &$order ) { + global $pmpro_currency; + + // Use this filter for other Add On compatibility that may use this filter. + do_action( 'pmpro_paypalexpress_session_vars' ); + + $params = array(); + $amount = $order->PaymentAmount; // This must change as well. + $amount_tax = $order->getTaxForPrice($amount); + $amount = round((float)$amount + (float)$amount_tax, 2); + + $amount = floatval($order->InitialPayment); /// This must change. + + $mode = pmpro_getOption("gateway_environment"); + if ($mode == 'sandbox') { + $key = pmpro_getOption("paystack_tsk"); + $pk = pmpro_getOption("paystack_tpk"); + } else { + $key = pmpro_getOption("paystack_lsk"); + $pk = pmpro_getOption("paystack_lpk"); + } + if ($key == '') { + echo "Api keys not set"; + } + + $koboamount = $amount*100; + + + $paystack_url = 'https://api.paystack.co/transaction/initialize'; + $headers = array( + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer '.$key + ); + + //Create Plan + $body = array( + 'email' => $order->Email, + 'amount' => $koboamount, + 'reference' => $order->code, + 'currency' => $pmpro_currency, + 'callback_url' => pmpro_url("confirmation", "?level=" . $order->membership_level->id), + 'metadata' => json_encode(array('custom_fields' => array( + array( + "display_name"=>"Plugin", + "variable_name"=>"plugin", + "value"=>"pm-pro" + ), + + ), 'custom_filters' => array("recurring" => true))), + ); + + // If the level is recurring only allow card payments for the subscription as other methods don't work. + $level = $order->getMembershipLevel(); + if ( pmpro_isLevelRecurring( $level ) ) { + $body['channels'] = array( 'card' ); + } + + $args = array( + 'body' => json_encode($body), + 'headers' => $headers, + 'timeout' => 60 + ); + + $request = wp_remote_post( $paystack_url, $args ); + // print_r($request); + if (!is_wp_error($request)) { + $paystack_response = json_decode( wp_remote_retrieve_body($request )); + if ($paystack_response->status){ + $url = $paystack_response->data->authorization_url; + wp_redirect($url); + exit; + } else { + wp_redirect(pmpro_url("checkout", "?level=" . $order->membership_level->id . "&error=" . $paystack_response->message)); + exit(); + } + } else { + wp_redirect(pmpro_url("checkout", "?level=" . $order->membership_level->id . "&error=Failed")); + exit(); + } + exit; + } + + /** + * Allow "Sync" with gateway for subscriptions. + * @since 1.7.2 + */ + public function update_subscription_info( $subscription ) { + $subscription_id = $subscription->get_subscription_transaction_id(); + $backtrace = self::get_caller_info(); + $furtherbacktrace = wp_debug_backtrace_summary(); + + $mode = get_option( 'pmpro_gateway_environment'); + if ( $mode == 'sandbox' ) { + $key = get_option( 'pmpro_paystack_tsk' ); + } else { + $key = get_option( 'pmpro_paystack_lsk' ); + } + + $paystack_url = 'https://api.paystack.co/subscription/' . $subscription_id; + + $headers = array( + 'Authorization' => 'Bearer ' . $key + ); + + $args = array( + 'headers' => $headers, + 'timeout' => 60, + ); + + $request = wp_remote_get( $paystack_url, $args ); + + // Request is okay, so let's get the data now and update what we need to. + if ( ! is_wp_error( $request ) ) { + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + if ( 200 !== wp_remote_retrieve_response_code( $request ) ) { + // Throw an error here from the API + return sprintf( esc_html__( 'Paystack error: %s', 'paystack-gateway-paid-memberships-pro' ), $response->message ); + } + + $update_array = array(); + $sub_info = $response->data; + + // The response status isn't active, so we're most likely already cancelled. + if ( $sub_info->status !== 'active' ) { + $update_array['status'] = 'cancelled'; // Does it + } else { + $update_array['status'] = 'active'; + } + + + // Let's make sure the cycle_numbers are correctly set based on the interval from Paystack. + switch( $sub_info->plan->interval ) { + case 'quarterly': + $update_array['cycle_number'] = 3; + break; + case 'biannually': + $update_array['cycle_number'] = 6; + break; + } + + // Update the subscription. + $update_array['next_payment_date'] = sanitize_text_field( $sub_info->next_payment_date ); // [YYYY]-[MM]-[DD + $update_array['startdate'] = sanitize_text_field( $sub_info->createdAt ); + $update_array['billing_amount'] = (float) $sub_info->amount/100; // Get currency value + $update_array['cycle_period'] = $this->convert_interval_for_pmpro( $sub_info->plan->interval ); // Convert interval for PMPro format (which sanitizes it) + $subscription->set( $update_array ); + } else { + return esc_html__( 'There was an error communicating with Paystack. Please confirm your connectivity and API details and try again.', 'paystack-gateway-paid-memberships-pro' ); + } + } + + function cancel(&$order, $update_status = true ) + { + $backtrace = self::get_caller_info(); + $furtherbacktrace = wp_debug_backtrace_summary(); + + //no matter what happens below, we're going to cancel the order in our system + if ( $update_status ) { + $order->updateStatus( "cancelled" ); + } + + $mode = pmpro_getOption("gateway_environment"); + $code = $order->subscription_transaction_id; + if ($mode == 'sandbox') { + $key = pmpro_getOption("paystack_tsk"); + } else { + $key = pmpro_getOption("paystack_lsk"); + + } + + if ( $code != "") { + $paystack_url = 'https://api.paystack.co/subscription/' . $code; + + $headers = array( + 'Authorization' => 'Bearer ' . $key + ); + $args = array( + 'headers' => $headers, + 'timeout' => 60, + ); + + $request = wp_remote_get($paystack_url, $args); + if (!is_wp_error($request) && 200 == wp_remote_retrieve_response_code($request)) { + $paystack_response = json_decode(wp_remote_retrieve_body($request)); + if ('active' == $paystack_response->data->status && $code == $paystack_response->data->subscription_code && '1' == $paystack_response->status) { + + $paystack_url = 'https://api.paystack.co/subscription/disable'; + $headers = array( + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer ".$key + ); + $body = array( + 'code' => $paystack_response->data->subscription_code, + 'token' => $paystack_response->data->email_token, + 'debug_trace'=> $backtrace . " ". $furtherbacktrace + ); + $args = array( + 'body' => json_encode($body), + 'headers' => $headers, + 'timeout' => 60, + ); + + $request = wp_remote_post($paystack_url, $args); + + if ( ! is_wp_error( $request ) ) { + return true; + } else { + return false; // There was an error cancelling for some reason. + } + } + } + } + return true; + } + + /** + * Allow refunds from within Paid Memberships Pro and Paystack. + * @since TBD + */ + public static function process_refund( $success, $order ) { + global $current_user; + + //default to using the payment id from the order + if ( !empty( $order->payment_transaction_id ) ) { + $transaction_id = $order->payment_transaction_id; + } + + //need a transaction id + if ( empty( $transaction_id ) ) { + return false; + } + + // OKAY do the refund now. + // Make the API call to PayStack to refund the order. + $mode = pmpro_getOption("gateway_environment"); + if ( $mode == "sandbox" ) { + $key = pmpro_getOption("paystack_tsk"); + + } else { + $key = pmpro_getOption("paystack_lsk"); + } + + $paystack_url = 'https://api.paystack.co/refund/'; + + $headers = array( + 'Authorization' => 'Bearer ' . $key, + 'Cache-Control' => 'no-cache' + ); + + // The transaction ID for the refund. + $fields = array( + 'transaction' => $transaction_id + ); + + $args = array( + 'headers' => $headers, + 'timeout' => 60, + 'body' => $fields + ); + + $success = false; + + // Try to make the API call now. + $request = wp_remote_post( $paystack_url, $args ); + + if ( ! is_wp_error( $request ) ) { + + $response = json_decode( wp_remote_retrieve_body( $request ) ); + + // If not successful throw an error. + if ( ! $response->status ) { + $order->notes = trim( $order->notes.' '.sprintf( __('Admin: Order refund failed on %1$s for transaction ID %2$s by %3$s. Order may have already been refunded.', 'paystack-gateway-paid-memberships-pro' ), date_i18n('Y-m-d H:i:s'), $transaction_id, $current_user->display_name ) ); + $order->saveOrder(); + } else { + // Set the order status to refunded and save it and return true + $order->status = 'refunded'; + + $success = true; + + $order->notes = trim( $order->notes.' '.sprintf( __('Admin: Order successfully refunded on %1$s for transaction ID %2$s by %3$s.', 'paystack-gateway-paid-memberships-pro' ), date_i18n('Y-m-d H:i:s'), $transaction_id, $current_user->display_name ) ); + + $user = get_user_by( 'id', $order->user_id ); + //send an email to the member + $myemail = new PMProEmail(); + $myemail->sendRefundedEmail( $user, $order ); + + //send an email to the admin + $myemail = new PMProEmail(); + $myemail->sendRefundedAdminEmail( $user, $order ); + + $order->saveOrder(); + } + } + + return $success; + } + + /** + * Enable refund functionality for paystack. + * @since TBD. + */ + static function pmpro_allowed_refunds_gateways( $gateways ) { + $gateways[] = 'paystack'; + return $gateways; + } + + /** + * Change the confirmation message, as Paystack's webhook notification may take a few seconds. + * @since TBD + */ + public static function pmpro_confirmation_incomplete_message( $message, $pmpro_invoice ) { + + if ( $pmpro_invoice->gateway != 'paystack' ) { + return $message; + } + + $message .= ' ' . esc_html__( 'This may take a few seconds.', 'text-domain' ); + + return $message; + } + + /** + * Undocumented function + * + * @param string $interval The pmpro paystack + * @return string $interval The required interval for PayStack to recognize. + */ + function convert_interval_for_paystack( $interval ) { + + $interval = strtolower( $interval ); + + switch( $interval ) { + case 'day': + $interval = 'daily'; + break; + case 'week': + $interval = 'weekly'; + break; + case 'month': + $interval = 'monthly'; + break; + case 'year': + $interval = 'annually'; + break; + default: + $interval = 'monthly'; + } + + return $interval; + + } + + /** + * Convert Paystack's intervals for PMPro's format. + * + * @param string $interval The received Paystack interval (i.e. Weekly, Monthly etc ) + * @return string $interval The converted interval for PMPro. + */ + function convert_interval_for_pmpro( $interval ) { + + $interval = strtolower( $interval ); + + switch( $interval ) { + case 'daily': + $interval = 'Day'; + break; + case 'weekly': + $interval = 'Week'; + break; + case 'monthly': + $interval = 'Month'; + break; + case 'annually': + $interval = 'Year'; + break; + case 'quarterly': + $interval = 'Month'; + break; + case 'biannually': + $interval = 'Month'; + break; + default: + $interval = 'Month'; + } + + return $interval; + + } + + // Get Caller info for debugging. + function get_caller_info() { + $c = ''; + $file = ''; + $func = ''; + $class = ''; + $trace = debug_backtrace(); + if (isset($trace[2])) { + $file = $trace[1]['file']; + $func = $trace[2]['function']; + if ((substr($func, 0, 7) == 'include') || (substr($func, 0, 7) == 'require')) { + $func = ''; + } + } else if (isset($trace[1])) { + $file = $trace[1]['file']; + $func = ''; + } + if (isset($trace[3]['class'])) { + $class = $trace[3]['class']; + $func = $trace[3]['function']; + $file = $trace[2]['file']; + } else if (isset($trace[2]['class'])) { + $class = $trace[2]['class']; + $func = $trace[2]['function']; + $file = $trace[1]['file']; + } + if ($file != '') $file = basename($file); + $c = $file . ": "; + $c .= ($class != '') ? ":" . $class . "->" : ""; + $c .= ($func != '') ? $func . "(): " : ""; + return($c); + } + +} // End of class PMProGateway_Paystack + + +/// Probably needs to go elsewhere? +/** + * Create the log string for debugging purposes. + * + * @param string $s The error/information you want to log to the IPN log. + * @return string $logstr A formatted message for the logfile. + * + * @since TBD + */ +function pmpro_paystack_webhook_log( $s ) { + global $logstr; + $logstr .= "\t" . $s . "\n"; +} + +/** + * Write to the log file and exit. + * + * @since TBD + */ +function pmpro_paystack_webhook_exit() { + global $logstr; + + //for log + if ( $logstr ) { + $logstr = "Logged On: " . date_i18n( "m/d/Y H:i:s" ) . "\n" . $logstr . "\n-------------\n"; + + echo esc_html( $logstr ); + + //log or dont log? log in file or email? + //- dont log if constant is undefined or defined but false + //- log to file if constant is set to TRUE or 'log' + //- log to file if constant is defined to a valid email address + if ( defined( 'PMPRO_PAYSTACK_WEBHOOK_DEBUG' ) ) { + if( PMPRO_PAYSTACK_WEBHOOK_DEBUG === false ){ + //dont log here. false mean no. + //should avoid counterintuitive interpretation of false. + } elseif ( PMPRO_PAYSTACK_WEBHOOK_DEBUG === "log" ) { + //file + + $logfile = apply_filters( 'pmpro_paystack_webhook_log_file', PMPRO_PAYSTACK_DIR . "logs/ipn.txt" ); + + // Check if the dir exists, if not let's create it. + $logdir = dirname( $logfile ); + if ( ! file_exists( $logdir ) ) { + mkdir( $logdir, 0775 ); + } + + // If the log file doesn't exist let's create it. + if ( ! file_exists( $logfile ) ) { + // Create a blank logfile + file_put_contents( $logfile, "" ); + } + + $loghandle = fopen( $logfile, "a+" ); + fwrite( $loghandle, $logstr ); + fclose( $loghandle ); + } elseif ( is_email( PMPRO_PAYSTACK_WEBHOOK_DEBUG ) ) { + //email to specified address + wp_mail( PMPRO_PAYSTACK_WEBHOOK_DEBUG, get_option( "blogname" ) . " IPN Log", nl2br( esc_html( $logstr ) ) ); + } else { + //email to admin + wp_mail( get_option( "admin_email" ), get_option( "blogname" ) . " IPN Log", nl2br( esc_html( $logstr ) ) ); + } + } + } + + exit; + +} diff --git a/class-paystack-plugin-tracker.php b/classes/paystack-tracker.php similarity index 100% rename from class-paystack-plugin-tracker.php rename to classes/paystack-tracker.php diff --git a/languages/paystack-gateway-paid-memberships-pro.mo b/languages/paystack-gateway-paid-memberships-pro.mo new file mode 100644 index 0000000..39f2451 Binary files /dev/null and b/languages/paystack-gateway-paid-memberships-pro.mo differ diff --git a/languages/paystack-gateway-paid-memberships-pro.po b/languages/paystack-gateway-paid-memberships-pro.po new file mode 100644 index 0000000..a4b8179 --- /dev/null +++ b/languages/paystack-gateway-paid-memberships-pro.po @@ -0,0 +1,231 @@ +# Copyright (C) 2025 Paid Memberships Pro, Paystack +# This file is distributed under the GPLv2 or later. +msgid "" +msgstr "" +"Project-Id-Version: Paystack Gateway for Paid Memberships Pro 1.9\n" +"Report-Msgid-Bugs-To: info@paidmembershipspro.com\n" +"Last-Translator: Paid Memberships Pro \n" +"Language-Team: Paid Memberships Pro \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2025-11-07T06:49:12+00:00\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"X-Generator: WP-CLI 2.12.0\n" +"X-Domain: paystack-gateway-paid-memberships-pro\n" + +#. Plugin Name of the plugin +#: class.pmprogateway_paystack.php +#: paystack-gateway-paid-memberships-pro.php +msgid "Paystack Gateway for Paid Memberships Pro" +msgstr "" + +#. Plugin URI of the plugin +#: class.pmprogateway_paystack.php +#: paystack-gateway-paid-memberships-pro.php +msgid "https://www.paidmembershipspro.com/add-ons/paystack-gateway/" +msgstr "" + +#. Description of the plugin +#: class.pmprogateway_paystack.php +#: paystack-gateway-paid-memberships-pro.php +msgid "Plugin to add Paystack payment gateway into Paid Memberships Pro" +msgstr "" + +#. Author of the plugin +msgid "Paystack, Paid Memberships Pro" +msgstr "" + +#. Author URI of the plugin +#: class.pmprogateway_paystack.php +#: paystack-gateway-paid-memberships-pro.php +msgid "https://www.paidmembershipspro.com" +msgstr "" + +#: class.pmprogateway_paystack.php:97 +#: class.pmprogateway_paystack.php:108 +#: class.pmprogateway_paystack.php:114 +#: class.pmprogateway_paystack.php:118 +#: classes/class.pmprogateway_paystack.php:205 +#: classes/class.pmprogateway_paystack.php:207 +msgid "Settings" +msgstr "" + +#: class.pmprogateway_paystack.php:111 +#: class.pmprogateway_paystack.php:122 +#: class.pmprogateway_paystack.php:128 +#: class.pmprogateway_paystack.php:132 +msgid "Check Out with Paystack" +msgstr "" + +#: class.pmprogateway_paystack.php:111 +#: class.pmprogateway_paystack.php:122 +#: class.pmprogateway_paystack.php:128 +#: class.pmprogateway_paystack.php:132 +msgid "Submit and Confirm" +msgstr "" + +#: class.pmprogateway_paystack.php:124 +#: class.pmprogateway_paystack.php:135 +#: class.pmprogateway_paystack.php:141 +#: class.pmprogateway_paystack.php:145 +msgid "Paystack" +msgstr "" + +#: class.pmprogateway_paystack.php:348 +#: class.pmprogateway_paystack.php:377 +#: class.pmprogateway_paystack.php:394 +#: class.pmprogateway_paystack.php:408 +#: class.pmprogateway_paystack.php:407 +#: classes/class.pmprogateway_paystack.php:142 +#: classes/class.pmprogateway_paystack.php:213 +#: classes/class.pmprogateway_paystack.php:144 +#: classes/class.pmprogateway_paystack.php:215 +msgid "Test Secret Key" +msgstr "" + +#: class.pmprogateway_paystack.php:356 +#: class.pmprogateway_paystack.php:385 +#: class.pmprogateway_paystack.php:402 +#: class.pmprogateway_paystack.php:416 +#: class.pmprogateway_paystack.php:415 +#: classes/class.pmprogateway_paystack.php:150 +#: classes/class.pmprogateway_paystack.php:221 +#: classes/class.pmprogateway_paystack.php:152 +#: classes/class.pmprogateway_paystack.php:223 +msgid "Test Public Key" +msgstr "" + +#: class.pmprogateway_paystack.php:364 +#: class.pmprogateway_paystack.php:393 +#: class.pmprogateway_paystack.php:410 +#: class.pmprogateway_paystack.php:424 +#: class.pmprogateway_paystack.php:423 +#: classes/class.pmprogateway_paystack.php:158 +#: classes/class.pmprogateway_paystack.php:229 +#: classes/class.pmprogateway_paystack.php:160 +#: classes/class.pmprogateway_paystack.php:231 +msgid "Live Secret Key" +msgstr "" + +#: class.pmprogateway_paystack.php:372 +#: class.pmprogateway_paystack.php:401 +#: class.pmprogateway_paystack.php:418 +#: class.pmprogateway_paystack.php:432 +#: class.pmprogateway_paystack.php:431 +#: classes/class.pmprogateway_paystack.php:166 +#: classes/class.pmprogateway_paystack.php:237 +#: classes/class.pmprogateway_paystack.php:168 +#: classes/class.pmprogateway_paystack.php:239 +msgid "Live Public Key" +msgstr "" + +#: class.pmprogateway_paystack.php:380 +#: class.pmprogateway_paystack.php:409 +#: class.pmprogateway_paystack.php:426 +#: class.pmprogateway_paystack.php:440 +#: class.pmprogateway_paystack.php:439 +#: classes/class.pmprogateway_paystack.php:174 +#: classes/class.pmprogateway_paystack.php:245 +#: classes/class.pmprogateway_paystack.php:176 +#: classes/class.pmprogateway_paystack.php:247 +msgid "Webhook" +msgstr "" + +#: class.pmprogateway_paystack.php:383 +#: class.pmprogateway_paystack.php:412 +#: class.pmprogateway_paystack.php:429 +#: class.pmprogateway_paystack.php:443 +#: class.pmprogateway_paystack.php:442 +#: classes/class.pmprogateway_paystack.php:177 +#: classes/class.pmprogateway_paystack.php:248 +#: classes/class.pmprogateway_paystack.php:179 +#: classes/class.pmprogateway_paystack.php:250 +msgid "To fully integrate with Paystack, be sure to use the following for your Webhook URL to" +msgstr "" + +#: class.pmprogateway_paystack.php:852 +#: class.pmprogateway_paystack.php:930 +#: class.pmprogateway_paystack.php:948 +#: class.pmprogateway_paystack.php:975 +#: class.pmprogateway_paystack.php:973 +#: class.pmprogateway_paystack.php:984 +msgid "Account" +msgstr "" + +#: class.pmprogateway_paystack.php:853 +#: class.pmprogateway_paystack.php:931 +#: class.pmprogateway_paystack.php:949 +#: class.pmprogateway_paystack.php:976 +#: class.pmprogateway_paystack.php:974 +#: class.pmprogateway_paystack.php:985 +msgid "Order" +msgstr "" + +#: class.pmprogateway_paystack.php:854 +#: class.pmprogateway_paystack.php:932 +#: class.pmprogateway_paystack.php:950 +#: class.pmprogateway_paystack.php:977 +#: class.pmprogateway_paystack.php:975 +#: class.pmprogateway_paystack.php:986 +msgid "Membership Level" +msgstr "" + +#: class.pmprogateway_paystack.php:855 +#: class.pmprogateway_paystack.php:933 +#: class.pmprogateway_paystack.php:951 +#: class.pmprogateway_paystack.php:978 +#: class.pmprogateway_paystack.php:976 +#: class.pmprogateway_paystack.php:987 +msgid "Amount Paid" +msgstr "" + +#. Author of the plugin +#: class.pmprogateway_paystack.php +#: paystack-gateway-paid-memberships-pro.php +msgid "Paid Memberships Pro, Paystack" +msgstr "" + +#: class.pmprogateway_paystack.php:1119 +#: class.pmprogateway_paystack.php:1140 +#: class.pmprogateway_paystack.php:1166 +#: class.pmprogateway_paystack.php:1164 +#: class.pmprogateway_paystack.php:1175 +#: classes/class.pmprogateway_paystack.php:484 +#: classes/class.pmprogateway_paystack.php:486 +msgid "There was an error communicating with Paystack. Please confirm your connectivity and API details and try again." +msgstr "" + +#. translators: %s: URL to the Paystack gateway documentation. +#: classes/class.pmprogateway_paystack.php:196 +#: classes/class.pmprogateway_paystack.php:198 +#, php-format +msgid "For detailed setup instructions, please visit our %s." +msgstr "" + +#: classes/class.pmprogateway_paystack.php:197 +#: classes/class.pmprogateway_paystack.php:199 +msgid "Paystack documentation" +msgstr "" + +#: classes/class.pmprogateway_paystack.php:266 +#: classes/class.pmprogateway_paystack.php:268 +msgid "With Paystack, members can pay using credit or debit cards, bank transfers, USSD, mobile money, QR codes, Apple Pay and more. Paystack is accepted worldwide and offers multi-currency support across numerous African countries." +msgstr "" + +#: classes/class.pmprogateway_paystack.php:608 +#: classes/class.pmprogateway_paystack.php:610 +#, php-format +msgid "Admin: Order refund failed on %1$s for transaction ID %2$s by %3$s. Order may have already been refunded." +msgstr "" + +#: classes/class.pmprogateway_paystack.php:616 +#: classes/class.pmprogateway_paystack.php:618 +#, php-format +msgid "Admin: Order successfully refunded on %1$s for transaction ID %2$s by %3$s." +msgstr "" + +#: classes/class.pmprogateway_paystack.php:455 +#, php-format +msgid "Paystack error: %s" +msgstr "" diff --git a/languages/paystack-gateway-paid-memberships-pro.pot b/languages/paystack-gateway-paid-memberships-pro.pot new file mode 100644 index 0000000..57eda1a --- /dev/null +++ b/languages/paystack-gateway-paid-memberships-pro.pot @@ -0,0 +1,107 @@ +# Copyright (C) 2025 Paid Memberships Pro, Paystack +# This file is distributed under the GPLv2 or later. +msgid "" +msgstr "" +"Project-Id-Version: Paystack Gateway for Paid Memberships Pro 1.9\n" +"Report-Msgid-Bugs-To: info@paidmembershipspro.com\n" +"Last-Translator: Paid Memberships Pro \n" +"Language-Team: Paid Memberships Pro \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2025-11-07T06:49:12+00:00\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"X-Generator: WP-CLI 2.12.0\n" +"X-Domain: paystack-gateway-paid-memberships-pro\n" + +#. Plugin Name of the plugin +#: paystack-gateway-paid-memberships-pro.php +msgid "Paystack Gateway for Paid Memberships Pro" +msgstr "" + +#. Plugin URI of the plugin +#: paystack-gateway-paid-memberships-pro.php +msgid "https://www.paidmembershipspro.com/add-ons/paystack-gateway/" +msgstr "" + +#. Description of the plugin +#: paystack-gateway-paid-memberships-pro.php +msgid "Plugin to add Paystack payment gateway into Paid Memberships Pro" +msgstr "" + +#. Author of the plugin +#: paystack-gateway-paid-memberships-pro.php +msgid "Paid Memberships Pro, Paystack" +msgstr "" + +#. Author URI of the plugin +#: paystack-gateway-paid-memberships-pro.php +msgid "https://www.paidmembershipspro.com" +msgstr "" + +#: classes/class.pmprogateway_paystack.php:144 +#: classes/class.pmprogateway_paystack.php:215 +msgid "Test Secret Key" +msgstr "" + +#: classes/class.pmprogateway_paystack.php:152 +#: classes/class.pmprogateway_paystack.php:223 +msgid "Test Public Key" +msgstr "" + +#: classes/class.pmprogateway_paystack.php:160 +#: classes/class.pmprogateway_paystack.php:231 +msgid "Live Secret Key" +msgstr "" + +#: classes/class.pmprogateway_paystack.php:168 +#: classes/class.pmprogateway_paystack.php:239 +msgid "Live Public Key" +msgstr "" + +#: classes/class.pmprogateway_paystack.php:176 +#: classes/class.pmprogateway_paystack.php:247 +msgid "Webhook" +msgstr "" + +#: classes/class.pmprogateway_paystack.php:179 +#: classes/class.pmprogateway_paystack.php:250 +msgid "To fully integrate with Paystack, be sure to use the following for your Webhook URL to" +msgstr "" + +#. translators: %s: URL to the Paystack gateway documentation. +#: classes/class.pmprogateway_paystack.php:198 +#, php-format +msgid "For detailed setup instructions, please visit our %s." +msgstr "" + +#: classes/class.pmprogateway_paystack.php:199 +msgid "Paystack documentation" +msgstr "" + +#: classes/class.pmprogateway_paystack.php:207 +msgid "Settings" +msgstr "" + +#: classes/class.pmprogateway_paystack.php:268 +msgid "With Paystack, members can pay using credit or debit cards, bank transfers, USSD, mobile money, QR codes, Apple Pay and more. Paystack is accepted worldwide and offers multi-currency support across numerous African countries." +msgstr "" + +#: classes/class.pmprogateway_paystack.php:455 +#, php-format +msgid "Paystack error: %s" +msgstr "" + +#: classes/class.pmprogateway_paystack.php:486 +msgid "There was an error communicating with Paystack. Please confirm your connectivity and API details and try again." +msgstr "" + +#: classes/class.pmprogateway_paystack.php:610 +#, php-format +msgid "Admin: Order refund failed on %1$s for transaction ID %2$s by %3$s. Order may have already been refunded." +msgstr "" + +#: classes/class.pmprogateway_paystack.php:618 +#, php-format +msgid "Admin: Order successfully refunded on %1$s for transaction ID %2$s by %3$s." +msgstr "" diff --git a/paystack-gateway-paid-memberships-pro.php b/paystack-gateway-paid-memberships-pro.php new file mode 100755 index 0000000..0942b56 --- /dev/null +++ b/paystack-gateway-paid-memberships-pro.php @@ -0,0 +1,124 @@ + 0 ? $set_y : $current_y; +$temp_m = $set_m > 0 ? $set_m : $current_m; +$temp_d = $set_d; + +// Add months. +if(!empty($add_months)) { +for($i = 0; $i < $add_months; $i++) { + // If "M1", only add months if current date of month has already passed. + if(0 == $i) { + if($temp_d < $current_d) { + $temp_m++; + $add_months--; + } + } else { + $temp_m++; + } + + // If we hit 13, reset to Jan of next year and subtract one of the years to add. + if($temp_m == 13) { + $temp_m = 1; + $temp_y++; + $add_years--; + } +} +} + +// Add years. +if(!empty($add_years)) { +for($i = 0; $i < $add_years; $i++) { + // If "Y1", only add years if current date has already passed. + if(0 == $i) { + $temp_date = strtotime(date("{$temp_y}-{$temp_m}-{$temp_d}")); + if($temp_date < $current_date) { + $temp_y++; + $add_years--; + } + } else { + $temp_y++; + } +} +} + +// Pad dates if necessary. +$temp_m = str_pad($temp_m, 2, '0', STR_PAD_LEFT); +$temp_d = str_pad($temp_d, 2, '0', STR_PAD_LEFT); + +// Put it all together. +$set_date = date("{$temp_y}-{$temp_m}-{$temp_d}"); + +// Make sure we use the right day of the month for dates > 28 +// From: http://stackoverflow.com/a/654378/1154321 +$dotm = pmpro_getMatches('/\-([0-3][0-9]$)/', $set_date, true); +if ( $temp_m == '02' && intval($dotm) > 28 || intval($dotm) > 30 ) { +$set_date = date('Y-m-t', strtotime(substr($set_date, 0, 8) . "01")); +} + + + +return $set_date; +} + diff --git a/pmpro-paystack-banner.jpg b/pmpro-paystack-banner.jpg new file mode 100644 index 0000000..e8b4e2c Binary files /dev/null and b/pmpro-paystack-banner.jpg differ diff --git a/readme.txt b/readme.txt index 0f60db0..692c534 100755 --- a/readme.txt +++ b/readme.txt @@ -1,10 +1,10 @@ === Paystack Gateway for Paid Membership Pro === -Contributors: paystack, kendysond, steveamaza, lukman008 +Contributors: paystack, kendysond, steveamaza, lukman008, andrewza, strangerstudios, paidmembershipspro Donate link: https://paystack.com/demo -Tags: paystack, recurrent payments, nigeria, mastercard, visa, target, Naira, payments, verve, paid membership pro -Requires at least: 3.1 -Tested up to: 5.9 -Stable tag: 1.7.0 +Tags: paid memberships pro, paystack, gateway, credit card, Naira +Requires at least: 5.2 +Tested up to: 6.8 +Stable tag: 1.9.1 License: GPLv3 License URI: http://www.gnu.org/licenses/gpl-3.0.html @@ -14,7 +14,7 @@ Pay with Paystack on Paid Membership Pro Paid Membership Pro is a complete member management and membership subscriptions plugin for WordPress. Paid Memberships Pro is designed for premium content sites, clubs/associations, subscription products, newsletters and more! -The **Paystack Gateway for Paid Membership Pro** allows site owners from Nigeria and Ghana to accept payments from their customers via Paid Membership Pro. +The **Paystack Gateway for Paid Membership Pro** allows site owners from Nigeria, Ghana, Kenya and South Africa to accept payments from their customers via Paid Membership Pro. To be able to use the Paystack Gateway for Paid Membership Pro, you must [have an account on Paystack](https://dashboard.paystack.com) from which you will get Test and Live API keys to connect the plugin to your Paystack business. Here are some benefits of using Paystack! @@ -47,21 +47,12 @@ We also have a developer community on Slack where we share product announcements = Minimum Requirements = * Confirm that your server can conclude a TLSv1.2 connection to Paystack's servers. More information about this requirement can be gleaned here: [TLS v1.2 requirement](https://developers.paystack.co/blog/tls-v12-requirement). -* Installed and activated Paid Membership Pro Plugin - -= Automatic installation = - -Automatic installation is the easiest option as WordPress handles the file transfers itself and you don’t need to leave your web browser. To do an automatic install of WPJobster Paystack Gateway, log in to your WordPress dashboard, navigate to the Plugins menu and click Add New. - -In the search field type "Paystack Gateway for Paid Membership Pro” and click Search Plugins. Once you’ve found our payment plugin you can view details about it such as the point release, rating and description. Most importantly of course, you can install it by simply clicking “Install Now”. +* Installed and activated [Paid Membership Pro Plugin] (https://www.paidmembershipspro.com) = Manual installation = The manual installation method involves downloading our payment plugin and uploading it to your webserver via your favourite FTP application. The WordPress codex contains [instructions on how to do this here](https://codex.wordpress.org/Managing_Plugins#Manual_Plugin_Installation). -= Updating = - -Automatic updates should work like a charm; as always though, ensure you backup your site just in case. == Frequently Asked Questions == @@ -71,17 +62,59 @@ You can find help and information on Paystack on our [Help Desk](https://paystac = Where can I get support or talk to other users? = -If you get stuck, you can ask for help in the [Paystack Gateway for Paid Membership Pro Plugin Forum](https://wordpress.org/support/plugin/paystack-gateway-paid-memberships-pro). You can also directly email support@paystack.com for assistance. +If you get stuck, you can ask for help in the [Paid Memberships Pro Support](https://www.paidmembershipspro.com/support). = Paystack Gateway for Paid Membership Pro is awesome! Can I contribute? = -Yes you can! Join in on our [GitHub repository](https://github.com/PaystackHQ/paystack-gateway-for-paid-memberships-pro) :) +Yes you can! Join in on our [GitHub repository](https://github.com/strangerstudios/paystack-gateway-paid-memberships-pro) == Screenshots == 1. The slick Paystack settings panel. == Changelog == += 1.9.1 - 2025-11-07 = +* BUG FIX: Fixed an issue where PMPro would show a warning that the gateway is not configured correctly when Paystack is set as the primary gateway. #14 (@andrewlimaza) +* BUG FIX: Fixed an issue with error output when syncing a subscription failed in the WordPress admin. #15 (@DAnn2012) + += 1.9 - 2025-07-11 = +* BUG FIX: Fixed an issue where the payment settings fields were not showing correctly in Paid Memberships Pro 3.5+ +* REFACTOR: Improved support for Paid Memberships Pro 3.0+ to better support new subscription features moving forward. + += 1.8 - 2024-11-06 = +* ENHANCEMENT: Serving updates from the paidmembershipspro.com update server. + += 1.7.8 - 2024-10-09 = +* BUG FIX: Fixed an issue where free checkouts would cause a fatal error. + += 1.7.7 - 2024-07-19 = +* ENHANCEMENT: Added support for Paid Memberships Pro V3.1+ and used `pmpro_get_element_class` method on the frontend. (@andrewlimaza) +* BUG FIX: Fixed an issue where cancellations weren't working correctly in some cases/instances. (@andrewlimaza) +* BUG FIX: Fixed an issue where billing would bill later in the day at the start of the subscription. (@andrewlimaza) + += 1.7.6 - 2024-07-03 = +* ENHANCEMENT: Added webhook logging functionality to debug incoming webhook data. You may use the constant 'PMPRO_PAYSTACK_WEBHOOK_LOG' to enable this feature. Please delete logs and disable this constant after completing debugging. +* BUG FIX: Fixed an issue where the webhook handler was not correctly storing the order amount if the subscription amount changed. +* BUG FIX: Fixed an issue when renewal payments were being processed and failing to complete the order. + += 1.7.4 - 2024-04-24 = +* ENHANCEMENT: Added new filter `pmpro_paystack_webhook_level` to tweak the level given to members after checkout. This includes support for the Set Expiration Dates Add On. + += 1.7.3 - 2024-04-15 = +* SECURITY: Improved sanitization to output of translatable strings. +* ENHANCEMENT: Added functionality for refunds within Paid Memberships Pro. Supports full refunds only. +* BUG FIX: Fixed an issue where subscriptions weren't being linked inside Paid Memberships Pro correctly when confirming the membership. + += 1.7.2 - 2024-02-15 = +* ENHANCEMENT: Added support for Paid Memberships Pro 3.0+ subscriptions. +* BUG FIX: Fixed an issue where discount codes were not reflecting on Paid Memberships Pro side. + += 1.7.1 - 2023-09-13 = +* SECURITY: Improved security to the webhook handler, this now checks for the presence of the Paystack signature header before processing the request. +* ENHANCEMENT: Only allow card checkout for recurring subscriptions as other payment options don't allow subscriptions. +* BUG FIX: Fixed an issue where intervals weren't being set correctly. This now supports all intervals, for quarterly and biannually please use 3 month and 6 month in the recurring subscription fields respectively. +* BUG FIX: Fixed an issue where non-expiring memberships would obtain an expiration date incorrectly. +* REFACTOR: Minor improvements to the settings UI page to align with other Paid Memberships Pro gateways. = 1.4 = * Add quarterly subscription option for recurring payments set to 3 months or 90 days cycle period. diff --git a/services/paystack-webhook.php b/services/paystack-webhook.php new file mode 100644 index 0000000..394e680 --- /dev/null +++ b/services/paystack-webhook.php @@ -0,0 +1,382 @@ +event ){ +case 'subscription.disable': + pmpro_handle_subscription_cancellation_at_gateway( $post_event->data->subscription_code, 'paystack', $gateway_environment ); + pmpro_paystack_webhook_exit(); + break; +case 'charge.success': // This runs for both recurring and initial checkouts. + $morder = new MemberOrder($post_event->data->reference); + $morder->getMembershipLevel(); + $morder->getUser(); + pmpro_paystack_complete_order( $post_event->data->reference, $morder ); + pmpro_paystack_confirm_subscription( $post_event, $morder ); // This will be for recurring subscriptions/levels. + $pstk_logger = new pmpro_paystack_plugin_tracker( 'pm-pro', $public_key ); + $pstk_logger->log_transaction_success( $post_event->data->reference ); + pmpro_paystack_webhook_log( 'Charge success. Reference: ' . $post_event->data->reference ); + break; +case 'invoice.create': + pmpro_paystack_renew_payment( $post_event ); + break; +case 'invoice.update': + pmpro_paystack_renew_payment($post_event); + break; +} +http_response_code(200); +pmpro_paystack_webhook_exit(); + + +/** + * Complete the PMPro order. + * + * @param [type] $reference + * @param [type] $morder + * @return void + */ +function pmpro_paystack_complete_order( $reference, &$order ) { + + // No reference passed, let's bail. + if ( empty( $reference ) ) { + return false; + } + + // Only run this if we got an order. + if ( !isset( $order->code ) ) { + return false; + } + + // If not object let's bail. + if ( ! is_object( $order ) ) { + return false; + } + + // Order is not in token status, so we can just bail - no need to update anything. + if ( $order->status != 'token' ) { + return false; + } + + // Reference is for another order + if ( $order->code != $reference ) { + return false; + } + + + // update order status and transaction ids + $order->payment_transaction_id = $reference; + $order->saveOrder(); // Temporarily save the order before processing it. + + // Change level and complete the order. + pmpro_pull_checkout_data_from_order( $order ); + return pmpro_complete_async_checkout( $order ); + +} + +// Confirm the subscription via plan etc. +function pmpro_paystack_confirm_subscription( $post_event, $order ) { + global $wpdb, $current_user, $pmpro_invoice, $pmpro_currency,$gateway; + + $webhook_reference_id = $post_event->data->reference; + + if (empty($pmpro_invoice)) { + $morder = new MemberOrder($webhook_reference_id); + if (!empty($morder) && $morder->gateway == "paystack") { + $pmpro_invoice = $morder; + } + } + + // No user found lets bail then. + if (!empty($pmpro_invoice) && $pmpro_invoice->gateway == "paystack" && isset($pmpro_invoice->total) && $pmpro_invoice->total > 0) { + $morder = $pmpro_invoice; + if ($morder->code == $webhook_reference_id ) { + + // TODO: Use pmpro_getLevel instead of a DB query. + $pmpro_level = $wpdb->get_row("SELECT * FROM $wpdb->pmpro_membership_levels WHERE id = '" . (int)$morder->membership_id . "' LIMIT 1"); + + // TODO: Move to pmpro_calculate_profile_start_date. + $startdate = apply_filters("pmpro_checkout_start_date", "'" . current_time("mysql") . "'", $morder->user_id, $pmpro_level); + + // The level object from the order that can be filtered when returning from Paystack and user is getting their membership level. + // TODO: Deprecate this. + $morder->membership_level = apply_filters( 'pmpro_paystack_webhook_level', $morder->membership_level, $morder->user_id ); + + // Get the mode of the environment so we know which keys to use. + $mode = pmpro_getOption("gateway_environment"); + if ($mode == "sandbox") { + $key = pmpro_getOption("paystack_tsk"); + $pk = pmpro_getOption("paystack_tpk"); + } else { + $key = pmpro_getOption("paystack_lsk"); + $pk = pmpro_getOption("paystack_lpk"); + } + $paystack_url = 'https://api.paystack.co/transaction/verify/' . $webhook_reference_id; + $headers = array( + 'Authorization' => 'Bearer ' . $key + ); + $args = array( + 'headers' => $headers, + 'timeout' => 60 + ); + $request = wp_remote_get( $paystack_url, $args ); + + if (!is_wp_error($request) && 200 == wp_remote_retrieve_response_code($request) ) { + $paystack_response = json_decode(wp_remote_retrieve_body($request)); + + if ( 'success' == $paystack_response->data->status ) { + $customer_code = $paystack_response->data->customer->customer_code; + + //Add logger here + $pstk_logger = new pmpro_paystack_plugin_tracker('pm-pro',$pk); + $pstk_logger->log_transaction_success($webhook_reference_id); + + // Get level from the order. + $pmpro_level = $morder->getMembershipLevel(); + + // There's recurring settings, lets convert to Paystack intervals now. + if ( $pmpro_level->billing_amount > 0 ) { + + // Convert the PMPro cycle to match that of paystacks. + $pmpro_paystack = new PMProGateway_paystack(); + $interval = $pmpro_paystack->convert_interval_for_paystack( $pmpro_level->cycle_period ); + + // Biannual and quarterly conversion for special cases. + if ( $pmpro_level->cycle_number == 3 && $pmpro_level->cycle_period == 'Month' ) { + $interval = 'quarterly'; + } + + if ( $pmpro_level->cycle_number == 6 && $pmpro_level->cycle_period == 'Month' ) { + $interval = 'biannually'; + } + + $amount = $pmpro_level->billing_amount; + $koboamount = $amount*100; + //Create Plan + $paystack_url = 'https://api.paystack.co/plan'; + $subscription_url = 'https://api.paystack.co/subscription'; + $check_url = 'https://api.paystack.co/plan?amount='.$koboamount.'&interval='.$interval; + $headers = array( + 'Content-Type' => 'application/json', + 'Authorization' => 'Bearer ' . $key + ); + + $checkargs = array( + 'headers' => $headers, + 'timeout' => 60 + ); + // Check if plan exist + $checkrequest = wp_remote_get($check_url, $checkargs); + if (!is_wp_error($checkrequest)) { + $response = json_decode(wp_remote_retrieve_body($checkrequest)); + if ($response->meta->total >= 1) { + $plan = $response->data[0]; + $plancode = $plan->plan_code; + + } else { + //Create Plan + $body = array( + 'name' => apply_filters( 'pmpro_paystack_plan_description', substr( trim( $pmpro_level->name ) . " at " . trim( get_bloginfo( "name" ) ), 0, 127 ), $pmpro_level ), + 'amount' => $koboamount, + 'interval' => $interval + ); + $args = array( + 'body' => json_encode($body), + 'headers' => $headers, + 'timeout' => 60 + ); + + $request = wp_remote_post($paystack_url, $args); + if (!is_wp_error($request)) { + $paystack_response = json_decode(wp_remote_retrieve_body($request)); + $plancode = $paystack_response->data->plan_code; + } + } + + } + + $subscription_delay = get_option( 'pmpro_subscription_delay_' . $pmpro_level->id, 0 ); + + $body = array( + 'customer' => $customer_code, + 'plan' => $plancode + ); + + if ( $subscription_delay ) { + if ( ! is_numeric( $subscription_delay ) ) { + $start_date = kkd_pmprosd_convert_date( $subscription_delay ); + } else { + $start_date = date( 'Y-m-d', strtotime( '+ ' . intval( $subscription_delay ) . ' Days', current_time( 'timestamp' ) ) ); + } + } else { + // $start_date = current_time( 'mysql' ); + $start_date = NULL; + } + + // If we are tweaking the start date via Subscription Delays Add On, let's set that to the subscription. + if ( ! empty( $start_date ) ) { + $body['start_date'] = apply_filters( 'pmpro_paystack_subscription_start_date', $start_date ); + } + + + $args = array( + 'body' => json_encode($body), + 'headers' => $headers, + 'timeout' => 60 + ); + + $request = wp_remote_post($subscription_url, $args); + if (!is_wp_error($request)) { + $paystack_response = json_decode(wp_remote_retrieve_body($request)); + if ( isset( $paystack_response->data->status ) && 'active' == $paystack_response->data->status ) { + $subscription_code = $paystack_response->data->subscription_code; + $token = $paystack_response->data->email_token; + $morder->subscription_transaction_id = $subscription_code; + $morder->subscription_token = $token; + $morder->saveOrder(); + } + } + } + + // get discount code (NOTE: but discount_code isn't set here. How to handle discount codes for PayPal Standard?) + $morder->getDiscountCode(); + if ( ! empty( $morder->discount_code ) ) { + // update membership level + $morder->getMembershipLevel( true ); + $discount_code_id = $morder->discount_code->id; + } else { + $discount_code_id = ''; + } + + // Get the expiration date. + if ( ! empty( $morder->membership_level->expiration_number ) ) { + $enddate = "'" . date_i18n( "Y-m-d", strtotime( "+ " . $morder->membership_level->expiration_number . " " . $morder->membership_level->expiration_period, current_time( "timestamp" ) ) ) . "'"; + } else { + $enddate = "NULL"; + } + + $custom_level = array( + 'user_id' => $morder->user_id, + 'membership_id' => $morder->membership_level->id, + 'code_id' => $discount_code_id, + 'initial_payment' => $morder->membership_level->initial_payment, + 'billing_amount' => $morder->membership_level->billing_amount, + 'cycle_number' => $morder->membership_level->cycle_number, + 'cycle_period' => $morder->membership_level->cycle_period, + 'billing_limit' => $morder->membership_level->billing_limit, + 'trial_amount' => $morder->membership_level->trial_amount, + 'trial_limit' => $morder->membership_level->trial_limit, + 'startdate' => $startdate, + 'enddate' => $enddate + ); + if ($morder->status != 'success') { + + if (pmpro_changeMembershipLevel($custom_level, $morder->user_id, 'changed')) { + $morder->membership_id = $morder->membership_level->id; + $morder->payment_transaction_id = $webhook_reference_id; + $morder->status = "success"; + $morder->saveOrder(); + } + + } + } + } + } + } +} + +/** + * Handle the successful recurring payments. + * + * @param object $post_event The Paystack event object data. + * @return void + */ +function pmpro_paystack_renew_payment( $post_event ) { + if ( isset( $post_event->data->paid ) && ( $post_event->data->paid == 1 ) ) { + $amount = $post_event->data->subscription->amount/100; + $subscription_code = $post_event->data->subscription->subscription_code; + $email = $post_event->data->customer->email; + $old_order = new MemberOrder(); + $old_order->getLastMemberOrderBySubscriptionTransactionID( $subscription_code ); + + if ( empty( $old_order ) || empty( $old_order->id ) ) { + pmpro_paystack_webhook_log( 'Could not find last order for subscription code: ' . $subscription_code ); + pmpro_paystack_webhook_exit(); + } + + $user_id = $old_order->user_id; + $user = get_userdata( $user_id ); + + if ( empty( $user ) ) { + pmpro_paystack_webhook_log( 'Could not get user for renewal payment' ); + pmpro_paystack_webhook_exit(); + } + + // Let's create the order. + $morder = new MemberOrder(); + $morder->user_id = $old_order->user_id; + $morder->membership_id = $old_order->membership_id; + $morder->subtotal = $amount; + $morder->total = $amount; + $morder->payment_transaction_id = ! empty( $post_event->data->invoice_code ) ? $post_event->data->invoice_code : ''; + $morder->subscription_transaction_id = $subscription_code; + // Set the orders date to time it was paid. + if ( ! empty( $post_event->data->paid_at ) ) { + $morder->timestamp = strtotime( sanitize_text_field( $post_event->data->paid_at ) ); + } + $morder->gateway = $old_order->gateway; + $morder->gateway_environment = $old_order->gateway_environment; + + // Save entire order data to IPN Log - loop through the order object. + $order_data = array(); + foreach ( $morder as $key => $value ) { + $order_data[ $key ] = $value; + } + + pmpro_paystack_webhook_log( 'Order data: ' . print_r( $order_data, true ) ); + + $morder->status = 'success'; + $morder->saveOrder(); + $morder->getMemberOrderByID($morder->id); + + //email the user their invoice + $pmproemail = new PMProEmail(); + $pmproemail->sendInvoiceEmail( $user, $morder ); + + do_action('pmpro_subscription_payment_completed', $morder); + pmpro_paystack_webhook_log( sprintf( 'Subscription payment completed for user with ID: %d. Order ID: %s', $user_id, $morder->code ) ); + pmpro_paystack_webhook_exit(); + } +} \ No newline at end of file diff --git a/style.css b/style.css deleted file mode 100644 index f688833..0000000 --- a/style.css +++ /dev/null @@ -1,434 +0,0 @@ -@charset "UTF-8"; -/* CSS Document */ -.pmpro_advanced_levels-genesis .pmpro_level.one-third, .pmpro_advanced_levels-genesis .pmpro_level.one-fourth, .pmpro_advanced_levels-gantry .pmpro_level.span4, .pmpro_advanced_levels-gantry .pmpro_level.span3, .pmpro_advanced_levels-woothemes .pmpro_level.threecol-one, .pmpro_advanced_levels-woothemes .pmpro_level.fourcol-one, .pmpro_advanced_levels-div .text-center, .pmpro_advanced_levels-compare_table th, .pmpro_advanced_levels-compare_table td {text-align: center; } -table.pmpro_advanced_levels-genesis .button, .pmpro_advanced_levels-genesis .pmpro_level.one-third .button, .pmpro_advanced_levels-genesis .pmpro_level.one-fourth .button, table.pmpro_advanced_levels-foundation .button, .pmpro_advanced_levels-foundation .pmpro_level.medium-4 .button, .pmpro_advanced_levels-foundation .pmpro_level.medium-3 .button, table.pmpro_advanced_levels-gantry .btn, table.pmpro_advanced_levels-woothemes .woo-sc-button, .pmpro_advanced_levels-woothemes .pmpro_level.threecol-one .woo-sc-button, .pmpro_advanced_levels-woothemes .pmpro_level.fourcol-one .woo-sc-button, table.pmpro_advanced_levels-twentyfourteen .button, .pmpro_advanced_levels-table .pmpro_btn, .text-center.pmpro_advanced_levels-div .pmpro_btn, table.pmpro_advanced_levels-bootstrap .btn, .pmpro_advanced_levels-compare_table .pmpro_btn, .pmpro_advanced_levels-compare_table_responsive .pmpro_btn, .pmpro_advanced_levels-compare_table_responsive .button {display: block; text-align: center; } -.pmpro_advanced_levels-compare_table {border: none; } -.pmpro_advanced_levels-compare_table th, .pmpro_advanced_levels-compare_table td {border-color: #d1d1d1; border-width: 0 1px 1px 0; padding: .75em 1em; vertical-align: middle; } -.pmpro_advanced_levels-compare_table thead th, .pmpro_advanced_levels-compare_table tfoot td {border-width: 0 1px 0 0; } -.pmpro_advanced_levels-compare_table tr th:last-child, .pmpro_advanced_levels-compare_table tr td:last-child {border-right: none; } -.pmpro_advanced_levels-compare_table thead tr th:nth-child(even), .pmpro_advanced_levels-compare_table tbody tr td:nth-child(even) {background: rgba(0,0,0,0.02); } -.pmpro_advanced_levels-compare_table thead tr th:first-child, .pmpro_advanced_levels-compare_table tfoot tr td:first-child {background: none; text-indent: -9999em; width: 20%; } -.pmpro_advanced_levels-compare_table thead tr:last-child th, .pmpro_advanced_levels-compare_table tbody tr:last-child td {border-bottom: .5rem solid #CCC; } -.pmpro_advanced_levels-compare_table tbody tr:nth-child(even) td {background: rgba(0,0,0,0.05); } -.pmpro_advanced_levels-compare_table tbody tr td:first-child {text-align: right; } -.pmpro_advanced_levels-compare_table tfoot td {padding: .75em 1em; vertical-align: middle; } -.pmpro_advanced_levels-compare_table thead th h2 {margin: 0; padding: .5em 0 0 0; } -.pmpro_advanced_levels-compare_table .pmpro_level-price {font-size: 1.6rem; padding-bottom: 0; padding-top: 0; } -.pmpro_advanced_levels-compare_table .pmpro_level-expiration {font-weight: normal; } -.pmpro_advanced_levels-compare_table .pmpro_level-compare-true, .pmpro_advanced_levels-compare_table_responsive .pmpro_level-compare-true {font-size: 2rem; line-height: 2rem; } -.pmpro_advanced_levels-compare_table .pmpro_level-compare-true:after, .pmpro_advanced_levels-compare_table_responsive .pmpro_level-compare-true:after {content: "\2713"; } -.pmpro_advanced_levels-compare_table .pmpro_level-compare-false, .pmpro_advanced_levels-compare_table_responsive .pmpro_level-compare-false {color: rgba(0,0,0,0.2); } -.pmpro_advanced_levels-compare_table .pmpro_level-compare-false:after, .pmpro_advanced_levels-compare_table_responsive .pmpro_level-compare-false:after {content: "\002D"; } - -/* Styles for Bootstrap Template */ -table.pmpro_advanced_levels-bootstrap h2 {margin-top: 0; } -.pmpro_advanced_levels-bootstrap .panel-heading h2 {margin: 0; padding: 0; } - -/* Styles for Genesis Template */ -table.pmpro_advanced_levels-genesis thead th, table.pmpro_advanced_levels-genesis tbody td {padding-right: 10px; padding-left: 10px; } - -/* Styles for Gantry Template */ -table.pmpro_advanced_levels-gantry h2 {border-bottom: none; box-shadow: none; padding-bottom: 0; } - -/* Styles for WooThemes Template */ -table.pmpro_advanced_levels-woothemes tbody td {vertical-align: middle; } - -/* Styles for Foundation Template */ -.pmpro_advanced_levels-foundation .panel .button {margin-bottom: 0; } -.pmpro_advanced_levels-foundation .panel h5.subheader {color: #6f6f6f; } -.pmpro_advanced_levels-foundation .pricing-table .price {font-size: 1rem; } -.pmpro_advanced_levels-foundation .pricing-table .price span {display: block; font-size: 2rem; } - -/* Styles for TwentyFourteen Template */ -.pmpro_advanced_levels-twentyfourteen .button, .pmpro_advanced_levels-twentyfourteen .button:hover {color: #FFF; } - -/* Styles for No Template */ -.pmpro_advanced_levels-div .post {padding: 1em; } - -.pmpro_advanced_levels-compare_table_responsive {display: none; text-align: center; } - -@media only screen and (max-width: 767px) { - .pmpro_advanced_levels-compare_table {display: none; } - .pmpro_advanced_levels-compare_table_responsive {display: block; } -} - -.pmpro_advanced_levels-div .row { - width: 100%; - margin-left: auto; - margin-right: auto; - margin-top: 0; - margin-bottom: 0; - max-width: 73.125em; } - .pmpro_advanced_levels-div .row:before, .pmpro_advanced_levels-div .row:after { - content: " "; - display: table; } - .pmpro_advanced_levels-div .row:after { - clear: both; } - .pmpro_advanced_levels-div .column, - .pmpro_advanced_levels-div .columns { - position: relative; - float: left; } - -.pmpro_advanced_levels-div [class*="column"] + [class*="column"]:last-child { - float: right; } - -.pmpro_advanced_levels-div [class*="column"] + [class*="column"].end { - float: left; } - -.pmpro_advanced_levels-div .small-12 { - width: 100%; } - -@media only screen and (min-width: 40.063em) { - .pmpro_advanced_levels-div .medium-1 { - width: 8.33333%; } - - .pmpro_advanced_levels-div .medium-2 { - width: 16.66667%; } - - .pmpro_advanced_levels-div .medium-3 { - width: 25%; } - - .pmpro_advanced_levels-div .medium-4 { - width: 33.33333%; } - - .pmpro_advanced_levels-div .medium-5 { - width: 41.66667%; } - - .pmpro_advanced_levels-div .medium-6 { - width: 50%; } - - .pmpro_advanced_levels-div .medium-7 { - width: 58.33333%; } - - .pmpro_advanced_levels-div .medium-8 { - width: 66.66667%; } - - .pmpro_advanced_levels-div .medium-9 { - width: 75%; } - - .pmpro_advanced_levels-div .medium-10 { - width: 83.33333%; } - - .pmpro_advanced_levels-div .medium-11 { - width: 91.66667%; } - - .pmpro_advanced_levels-div .medium-12 { - width: 100%; } -} - -/*-------------------------------------------------------------- -11.1 Paid Memberships Pro Integrated Styles ---------------------------------------------------------------*/ -#pmpro_levels {margin-top: 2rem; margin-bottom: 2rem; } -#pmpro_levels .medium-4, #pmpro_levels .medium-3 { - text-align: center; -} -#pmpro_levels .row .post { - padding: 0 1rem; -} -#pmpro_levels .post h2, .memberlite_signup h2, .pmpro_signup_form h2 { - background: #FAFAFA; - color: #2C3E50; - border-top: 1px solid #CCC; - border-bottom: 1px dotted #CCC; - padding: .5rem; - margin: 0 0 1rem 0; -} -#pmpro_levels .medium-4 .pmpro_btn, #pmpro_levels .medium-3 .pmpro_btn, .pmpro_levels-table .pmpro_btn, .pmpro_advanced_levels-compare_table .pmpro_btn, .pmpro_advanced_levels-compare_table_responsive .pmpro_btn { - display: block; -} -#pmpro_levels.pmpro_levels-table.pmpro_level-highlight, #pmpro_levels.pmpro_levels-div .pmpro_level-highlight, #pmpro_levels.pmpro_levels-2col .pmpro_level-highlight, #pmpro_levels.pmpro_levels-3col .pmpro_level-highlight, #pmpro_levels.pmpro_levels-4col .pmpro_level-highlight, .memberlite_signup, .pmpro_signup_form {padding: 1rem; background: #FFF; z-index: 100; border-top: .5rem solid #18BC9C; border-bottom: 1.5rem solid #18BC9C; border-left: 1px solid #CCC; border-right: 1px solid #CCC; } -#pmpro_levels.pmpro_levels-2col .pmpro_level-highlight, #pmpro_levels.pmpro_levels-3col .pmpro_level-highlight, #pmpro_levels.pmpro_levels-4col .pmpro_level-highlight {margin-top: -1.5rem; } - -#pmpro_levels_table td, #pmpro_levels.pmpro_levels-table td {vertical-align: middle; } - -#pmpro_levels.pmpro_levels-table .pmpro_level-highlight td:first-child {border-left: 15px solid #18BC9C } -#pmpro_levels.pmpro_levels-table .pmpro_level-highlight td:last-child {border-right: 15px solid #18BC9C } - -#pmpro_levels.pmpro_advanced_levels-compare_table {overflow: hidden; } -#pmpro_levels.pmpro_advanced_levels-compare_table th, #pmpro_levels.pmpro_advanced_levels-compare_table td {padding: 1rem 2rem; position: relative; text-align: center; } -#pmpro_levels.pmpro_advanced_levels-compare_table thead th {border: none; padding-top: 0; text-align: center; } -#pmpro_levels.pmpro_advanced_levels-compare_table thead th h2 {margin-bottom: 0; } -#pmpro_levels.pmpro_advanced_levels-compare_table thead th:first-child, #pmpro_levels.pmpro_advanced_levels-compare_table tfoot td:first-child {background: none; border: none; text-indent: -9999em; width: 20%; } -#pmpro_levels.pmpro_advanced_levels-compare_table thead tr th:nth-child(even) {background: rgba(0,0,0,0.1); } -#pmpro_levels.pmpro_advanced_levels-compare_table tbody td {vertical-align: middle; } -#pmpro_levels.pmpro_advanced_levels-compare_table tbody td:first-child {text-align: right; } -#pmpro_levels.pmpro_advanced_levels-compare_table tbody tr:last-child td {border-bottom: .5rem solid #CCC; } -#pmpro_levels.pmpro_advanced_levels-compare_table thead tr:last-child th {border-bottom: .5rem solid #CCC; } -#pmpro_levels.pmpro_advanced_levels-compare_table thead th.pmpro_level-highlight, #pmpro_levels.pmpro_advanced_levels-compare_table tbody td.pmpro_level-highlight, #pmpro_levels.pmpro_advanced_levels-compare_table tfoot td.pmpro_level-highlight { border-left: 1rem solid #18BC9C; border-right: 1rem solid #18BC9C; } -#pmpro_levels.pmpro_advanced_levels-compare_table thead tr:first-child th.pmpro_level-highlight { border-top: 1rem solid #18BC9C; } -#pmpro_levels.pmpro_advanced_levels-compare_table thead tr:last-child th.pmpro_level-highlight, #pmpro_levels.pmpro_advanced_levels-compare_table tbody tr td.pmpro_level-highlight, #pmpro_levels.pmpro_advanced_levels-compare_table tfoot tr td.pmpro_level-highlight { border-bottom: none; } -#pmpro_levels.pmpro_advanced_levels-compare_table tfoot tr:last-child td.pmpro_level-highlight {border-bottom: 1rem solid #18BC9C; } - -#pmpro_levels.pmpro_advanced_levels-compare_table_responsive {display: none; } -#pmpro_levels.pmpro_advanced_levels-compare_table_responsive {text-align: center; } -#pmpro_levels.pmpro_advanced_levels-compare_table_responsive .pmpro_level-highlight {border-top: .5rem solid #18BC9C; border-bottom: 1.5rem solid #18BC9C; border-left: 1px solid #CCC; border-right: 1px solid #CCC; } - -#pmpro_levels.pmpro_advanced_levels-compare_table:hover tbody tr:nth-child(even) td {background: none !important; } -#pmpro_levels.pmpro_advanced_levels-compare_table tbody tr:hover {background-color: rgba(252,248,227,0.8); } -#pmpro_levels.pmpro_advanced_levels-compare_table tbody td:hover::after, #pmpro_levels.pmpro_advanced_levels-compare_table tbody tr:nth-child(even) td:hover::after { - background-color: rgba(252,248,227,0.3); - content: ""; - height: 10000px; - left: 0; - position: absolute; - top: -5000px; - width: 100%; - z-index: -1; -} - -.pmpro_levels-3col .pmpro_level-price, .pmpro_levels-4col .pmpro_level-price {font-size: 24px; font-size: 2.4rem; } -.pmpro_levels-3col .pmpro_level-subprice, .pmpro_levels-4col .pmpro_level-subprice {display: block; font-size: 14px; font-size: 1.4rem; line-height: 2rem; } -.pmpro_levels-3col .pmpro_level-trialprice, .pmpro_levels-4col .pmpro_level-trialprice {display: block; font-size: 14px; font-size: 1.4rem; line-height: 2rem; margin: 1rem; } -.pmpro_levels-div .pmpro_level-subprice, .pmpro_levels-table .pmpro_level-subprice { } -.pmpro_levels-3col .pmpro_level-expiration, .pmpro_levels-4col .pmpro_level-expiration { } - -.pmpro_levels-div .pmpro_btn-select, .pmpro_levels-2col .pmpro_btn-select {margin-left: 3rem; } -.pmpro_levels-div .pmpro_level-price, .pmpro_levels-2col .pmpro_level-price, .pmpro_levels-div .pmpro_level-expiration, .pmpro_levels-2col .pmpro_level-expiration {display: inline; margin: 0; } - -.memberlite_signup-fixed {position: fixed; top: 0; } - -.pmpro_asterisk {color: #C00; } -.pmpro_asterisk abbr {border: none; } -form.pmpro_form table {margin: 0 0 2.5rem; } -form.pmpro_form #pmpro_level_cost {margin: 0; } -form.pmpro_form p {margin: 1rem 0; } -form.pmpro_form label {font-weight: 400; } -form.pmpro_form span.pmpro_thead-name {width: 35%; } -form.pmpro_form span.pmpro_thead-msg {font-family: 'Lato', sans-serif; font-weight: normal; width: 65%; } -form.pmpro_form .pmpro_submit {padding: 0 1rem; } -form.pmpro_form textarea {width:75%; } -form.pmpro_form .pmpro_checkout { - padding-top: 0; -} -form.pmpro_form .pmpro_checkout h2 { - background: #EFEFEF; - border-top: 1px solid #CCC; - border-bottom: 1px solid #CCC; - font-family: 'Lato', sans-serif; - font-size: 16px; - font-size: 1.6rem; - font-weight: 700; - line-height: 2.6rem; - margin: 0; - padding: 1rem; -} -form.pmpro_form div.pmpro_checkout h2:after { - clear: both; - content: ''; - display: table; -} -form.pmpro_form .pmpro_checkout .pmpro_checkout-fields { - padding: 1rem; -} -form.pmpro_form .pmpro_checkout #other_discount_code_p, form.pmpro_form .pmpro_checkout #other_discount_code_div { - border-top: 1px dotted #CCC; - margin: 1rem 0 0 0; - padding: 1rem 0 0 0; -} -form.pmpro_form .pmpro_checkout #other_discount_code_div input[type=button], #discount_code_button {padding-top: 3px; padding-bottom: 3px; } - -#pmpro_account .pmpro_box {padding: 3rem 0 0 0; margin: 0 0 3rem 0; border-bottom: none; border-top: 1px dotted #CCC; } -#pmpro_account #pmpro_account-membership .pmpro_actionlinks {text-align: center; } -#pmpro_account .pmpro_actionlinks a {text-decoration: underline; } -#pmpro_account #pmpro_account-membership table tbody tr td .pmpro_actionlinks {text-align: left; } - -#pmpro_account .pmpro_box h3 {margin: 0 0 2rem 0; } -#pmpro_account #pmpro_account-profile ul {margin-top: 1rem; margin-bottom: 1rem; } - -.pmpro_member_directory h3 {margin-top: 0; } - -/*-------------------------------------------------------------- -11.2 Theme My Login: https://wordpress.org/plugins/theme-my-login/ ---------------------------------------------------------------*/ -#your-profile label {display: inline-block; } -#your-profile .form-table th {width: 250px; } -#your-profile h3 { - background: #EFEFEF; - border-top: 1px solid #CCC; - border-bottom: 1px solid #CCC; - margin: 0; - padding: 1rem; -} -#your-profile p {margin-bottom: .5rem; } -#your-profile p.submit {text-align: center; } -#secondary .widget_theme_my_login .widget-title {margin-bottom: .5em; } -#secondary .widget_theme_my_login ul li { - border: none; - padding: 0; -} -#secondary .tml {max-width: 100%; margin: 0 .5em; } -#secondary .tml p {margin-bottom: 1rem; } -#secondary .tml .input {margin-top: 0; } -.tml-user-avatar { - float: left; - width: 80px; - height: 80px; - border-radius: 50%; - margin: 0 10px 0 0; -} -.tml-user-avatar img { - border-radius: 50%; - transition: transform 0.5s ease; - transform: scale(0.8); -} -.tml .tml-action-links { - border-top: 1px dotted #AAA; - list-style: none; - margin: 1rem 0 0 0; - padding: 1rem 0 0 0; - width: 100%; -} -.tml .tml-action-links li { - display:inline-block; -} -#primary .tml-action-links { - background: #FAFAFA; - border-bottom: 1px dotted #CCC; - color: #AAA; - font-style: italic; - padding: 1rem; -} -#primary .tml-action-links a { - border-bottom: 1px solid transparent; - color: #777; - text-decoration: none; -} -#primary .tml-action-links a:hover {border-bottom: 1px dotted #777; } -#secondary .tml-action-links {text-align: center; } -ul.tml-user-links {list-style: none; margin-bottom: 0; } - -#primary .medium-4 .login input[type="text"], #primary .medium-4 .login input[type="password"] {width: 100%; } -#primary .medium-4 .login #pass-strength-result {width: 100%; } -#primary .medium-4 .login input[type="submit"] {display: block; width: 100%; } -#primary .medium-4 .tml-action-links {text-align: center; } - - -/*-------------------------------------------------------------- -4.1 Buttons ---------------------------------------------------------------*/ -button, -input, -select, -textarea, -a.comment-reply-link, -a.pmpro_btn, -#main div.em-search-main button.em-search-submit { - font-size: 100%; /* Corrects font size not being inherited in all browsers */ - margin: 0; /* Addresses margins set differently in IE6/7, F3/4, S5, Chrome */ - vertical-align: baseline; /* Improves appearance and consistency in all browsers */ -} -button, -input[type="button"], -input[type="reset"], -input[type="submit"], -.btn, -.btn:link, -a.comment-reply-link, -a.pmpro_btn, -.pmpro_content_message a, -.pmpro_content_message a:link, -input[type="submit"].pmpro_btn, -#main div.em-search-main button.em-search-submit { - background: #95a5a6; - border: none; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - box-shadow: none; - color: #FFF !important; - cursor: pointer; /* Improves usability and consistency of cursor style between image-type 'input' and others */ - /* -webkit-appearance: button; Corrects inability to style clickable 'input' types in iOS */ - font-family: 'Lato', sans-serif; - font-size: 16px; - font-size: 1.6rem; - font-style: normal; - font-weight: normal; - display: inline-block; - /*padding: 1rem 1.5rem;*/ - text-align: center; - text-decoration: none; - text-shadow: none; - -webkit-transform: translateZ(0); - transform: translateZ(0); - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -moz-osx-font-smoothing: grayscale; - overflow: hidden; - -webkit-transition-duration: 0.3s; - transition-duration: 0.3s; - -webkit-transition-property: color, background-color; - transition-property: color, background-color; -} -#main div.em-search-main button.em-search-submit {padding-bottom: .5rem; } - -a.pmpro_btn, -a.pmpro_btn:link, -a.pmpro_btn:visited, -input[type="submit"].pmpro_btn { - background: #F39C12; - border: none; - color: #FFF; - font-size: 18px; - font-size: 1.8rem; - padding: 0.4rem 1rem; - margin-top: 1em; - text-shadow: none; -} -button:hover, -input[type="button"]:hover, -input[type="reset"]:hover, -input[type="submit"]:hover, -button:focus, -input[type="button"]:focus, -input[type="reset"]:focus, -input[type="submit"]:focus, -button:active, -input[type="button"]:active, -input[type="reset"]:active, -input[type="submit"]:active, -.btn:hover, -.btn:active, -.btn:focus, -a.comment-reply-link:focus, -a.comment-reply-link:active, -a.comment-reply-link:hover, -a.pmpro_btn:active, -#main div.em-search-main button.em-search-submit:hover { - background: #798d8f; - color: #FFF; -} - -a.pmpro_btn:hover, input[type="submit"].pmpro_btn:hover { - background: #d90000; - border: none; - text-shadow: none; -} - -.btn_info, .btn_info:link {background-color: #5bc0de; } -.btn_success, .btn_success:link {background-color: #5cb85c; } -.btn_error, .btn_error:link {background-color: #d9534f; } -.btn_alert, .btn_alert:link {background-color: #f0ad4e; } -.btn_info:hover {background-color: #31b0d5; } -.btn_success:hover {background-color: #449d44; } -.btn_error:hover {background-color: #c9302c; } -.btn_alert:hover {background-color: #ec971f; } - -.btn_link { - cursor: pointer; /* Improves usability and consistency of cursor style between image-type 'input' and others */ - display: inline-block; - /* -webkit-appearance: button; Corrects inability to style clickable 'input' types in iOS */ - font-family: 'Lato', sans-serif; - font-style: normal; - font-weight: 400; - overflow: hidden; - padding: 1rem 1.5rem; - text-decoration: none; - text-shadow: none; -} -.btn_link:hover {text-decoration: underline; } -.btn_primary, .btn_primary:link {background: #2C3E50; } -.btn_secondary, .btn_secondary:link {background: #18BC9C; } -.btn_action, .btn_action:link {background: #F39C12; } -.btn_block, .btn_block:link {display: block; width: 100%; } -.btn .fa {padding: 0 .5rem; }