all atb changes
This commit is contained in:
50
app/plugins/members/controllers/admin/invoices.php
Normal file
50
app/plugins/members/controllers/admin/invoices.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* app/plugins/members/controllers/admin/members/blog.php
|
||||
*
|
||||
* This is the Membership Invoices admin controller.
|
||||
*
|
||||
* @package TP Members
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Controllers\Admin;
|
||||
|
||||
use TheTempusProject\Houdini\Classes\Views;
|
||||
use TheTempusProject\Houdini\Classes\Navigation;
|
||||
use TheTempusProject\Houdini\Classes\Components;
|
||||
use TheTempusProject\Classes\AdminController;
|
||||
use TheTempusProject\Models\MembershipRecords as MemberModel;
|
||||
|
||||
class Invoices extends AdminController {
|
||||
public static $memberships;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
self::$title = 'Admin - Memberships';
|
||||
self::$memberships = new MemberModel;
|
||||
$view = Navigation::activePageSelect( 'nav.admin', '/admin/member' );
|
||||
Components::set( 'ADMINNAV', $view );
|
||||
}
|
||||
|
||||
public function index( $data = null ) {
|
||||
Views::view( 'members.admin.list', self::$memberships->list() );
|
||||
}
|
||||
|
||||
public function create( $data = null ) {
|
||||
}
|
||||
|
||||
public function edit( $data = null ) {
|
||||
}
|
||||
|
||||
public function view( $data = null ) {
|
||||
}
|
||||
|
||||
public function delete( $data = null ) {
|
||||
}
|
||||
|
||||
public function preview( $data = null ) {
|
||||
}
|
||||
}
|
75
app/plugins/members/controllers/admin/members.php
Normal file
75
app/plugins/members/controllers/admin/members.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* app/plugins/blog/controllers/admin/blog.php
|
||||
*
|
||||
* This is the Blog admin controller.
|
||||
*
|
||||
* @package TP Blog
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Controllers\Admin;
|
||||
|
||||
use TheTempusProject\Houdini\Classes\Views;
|
||||
use TheTempusProject\Houdini\Classes\Navigation;
|
||||
use TheTempusProject\Houdini\Classes\Components;
|
||||
use TheTempusProject\Classes\AdminController;
|
||||
use TheTempusProject\Plugins\Members as MemberModel;
|
||||
use TheTempusProject\Houdini\Classes\Issues;
|
||||
use TheTempusProject\Bedrock\Functions\Input;
|
||||
use TheTempusProject\Hermes\Functions\Route as Routes;
|
||||
use TheTempusProject\Hermes\Functions\Redirect;
|
||||
|
||||
class Members extends AdminController {
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
$view = Navigation::activePageSelect( 'nav.admin', '/admin/member' );
|
||||
Components::set( 'ADMINNAV', $view );
|
||||
}
|
||||
|
||||
public function index( $data = null ) {
|
||||
self::$title = 'Admin - Membership Scripts';
|
||||
Views::view( 'members.admin.scripts' );
|
||||
}
|
||||
|
||||
public function orphans( $data = null, $id = null ) {
|
||||
self::$title = 'Admin - Orphaned PRoducts';
|
||||
if ( $data = 'abandon' && ! empty( $id ) ) {
|
||||
MemberModel::orphanAbandon( $id );
|
||||
Redirect::to('admin/members/orphans');
|
||||
}
|
||||
$orphans = MemberModel::findOrphans();
|
||||
Views::view( 'members.admin.orphans', $orphans );
|
||||
}
|
||||
|
||||
public function webhooks( $data = null, $id = null ) {
|
||||
self::$title = 'Admin - Membership Webhooks';
|
||||
|
||||
if ( $data = 'delete' && ! empty( $id ) ) {
|
||||
MemberModel::webhookRemove( $id );
|
||||
Redirect::to('admin/members/webhooks');
|
||||
}
|
||||
|
||||
$data = [];
|
||||
$webhooks = MemberModel::webhookList();
|
||||
foreach ($webhooks->data as $key => $webhook) {
|
||||
$hook = new \stdClass;
|
||||
$hook->id = $webhook->id;
|
||||
$hook->enabled_events = implode( ', <br>', $webhook->enabled_events );
|
||||
$hook->status = $webhook->status;
|
||||
$hook->url = $webhook->url;
|
||||
$data[] = $hook;
|
||||
}
|
||||
Components::set( 'urltouse', Routes::getAddress() );
|
||||
|
||||
if ( !Input::exists( 'submit' ) ) {
|
||||
return Views::view( 'members.admin.webhooks', $data );
|
||||
}
|
||||
MemberModel::webhookSetup();
|
||||
Issues::add( 'success', 'Webhooks Generated' );
|
||||
Issues::add( 'error', 'Now, LEAVE!' );
|
||||
Redirect::to('admin/members/webhooks');
|
||||
}
|
||||
}
|
97
app/plugins/members/controllers/admin/products.php
Normal file
97
app/plugins/members/controllers/admin/products.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
/**
|
||||
* app/plugins/members/controllers/admin/members/products.php
|
||||
*
|
||||
* This is the Membership Products admin controller.
|
||||
*
|
||||
* @package TP Members
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Controllers\Admin;
|
||||
|
||||
use TheTempusProject\Houdini\Classes\Views;
|
||||
use TheTempusProject\Houdini\Classes\Navigation;
|
||||
use TheTempusProject\Houdini\Classes\Components;
|
||||
use TheTempusProject\Classes\AdminController;
|
||||
use TheTempusProject\Models\MembershipProducts;
|
||||
use TheTempusProject\Bedrock\Functions\Input;
|
||||
use TheTempusProject\Classes\Forms;
|
||||
use TheTempusProject\Houdini\Classes\Issues;
|
||||
use TheTempusProject\Bedrock\Functions\Check;
|
||||
|
||||
class Products extends AdminController {
|
||||
public static $products;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
self::$title = 'Admin - Membership Products';
|
||||
self::$products = new MembershipProducts;
|
||||
$view = Navigation::activePageSelect( 'nav.admin', '/admin/products' );
|
||||
Components::set( 'ADMINNAV', $view );
|
||||
}
|
||||
|
||||
public function index( $data = null ) {
|
||||
Views::view( 'members.admin.products.list', self::$products->list() );
|
||||
}
|
||||
|
||||
public function create( $data = null ) {
|
||||
if ( !Input::exists( 'submit' ) ) {
|
||||
return Views::view( 'members.admin.products.create' );
|
||||
}
|
||||
if ( !Forms::check( 'newMembershipProduct' ) ) {
|
||||
Issues::add( 'error', [ 'There was an error with your request.' => Check::userErrors() ] );
|
||||
return $this->index();
|
||||
}
|
||||
$result = self::$products->create( Input::post( 'name' ), Input::post( 'description' ), Input::post( 'monthly_price' ), Input::post( 'yearly_price' ) );
|
||||
if ( $result ) {
|
||||
Issues::add( 'success', 'Your product has been created.' );
|
||||
return $this->index();
|
||||
} else {
|
||||
Issues::add( 'error', [ 'There was an unknown error submitting your data.' => Check::userErrors() ] );
|
||||
return $this->index();
|
||||
}
|
||||
}
|
||||
|
||||
public function edit( $id = null ) {
|
||||
if ( !Input::exists( 'submit' ) ) {
|
||||
return Views::view( 'members.admin.products.edit', self::$products->findById( $id ) );
|
||||
}
|
||||
if ( !Forms::check( 'editMembershipProduct' ) ) {
|
||||
Issues::add( 'error', [ 'There was an error with your form.' => Check::userErrors() ] );
|
||||
return $this->index();
|
||||
}
|
||||
|
||||
if ( self::$products->updatePost( $id, Input::post( 'name' ), Input::post( 'description' ), Input::post( 'monthly_price' ), Input::post( 'yearly_price' ) ) === true ) {
|
||||
Issues::add( 'success', 'Your product Updated.' );
|
||||
return $this->index();
|
||||
}
|
||||
Issues::add( 'error', 'There was an error with your request.' );
|
||||
$this->index();
|
||||
}
|
||||
|
||||
public function view( $id = null ) {
|
||||
$data = self::$products->findById( $id );
|
||||
if ( $data !== false ) {
|
||||
return Views::view( 'members.admin.products.view', $data );
|
||||
}
|
||||
Issues::add( 'error', 'Product not found.' );
|
||||
$this->index();
|
||||
}
|
||||
|
||||
public function delete( $data = null ) {
|
||||
if ( $data == null ) {
|
||||
if ( Input::exists( 'MP_' ) ) {
|
||||
$data = Input::post( 'MP_' );
|
||||
}
|
||||
}
|
||||
if ( !self::$products->delete( (array) $data ) ) {
|
||||
Issues::add( 'error', 'There was an error with your request.' );
|
||||
} else {
|
||||
Issues::add( 'success', 'Post has been deleted' );
|
||||
}
|
||||
$this->index();
|
||||
}
|
||||
}
|
56
app/plugins/members/controllers/admin/records.php
Normal file
56
app/plugins/members/controllers/admin/records.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* app/plugins/members/controllers/admin/members/records.php
|
||||
*
|
||||
* This is the Membership Records admin controller.
|
||||
*
|
||||
* @package TP Members
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Controllers\Admin;
|
||||
|
||||
use TheTempusProject\Houdini\Classes\Views;
|
||||
use TheTempusProject\Houdini\Classes\Navigation;
|
||||
use TheTempusProject\Houdini\Classes\Components;
|
||||
use TheTempusProject\Classes\AdminController;
|
||||
use TheTempusProject\Models\Memberships as MemberModel;
|
||||
|
||||
class Records extends AdminController {
|
||||
public static $memberships;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
self::$title = 'Admin - Memberships';
|
||||
self::$memberships = new MemberModel;
|
||||
$view = Navigation::activePageSelect( 'nav.admin', '/admin/member' );
|
||||
Components::set( 'ADMINNAV', $view );
|
||||
}
|
||||
|
||||
public function index( $data = null ) {
|
||||
Views::view( 'members.admin.memberships.list', self::$memberships->list() );
|
||||
}
|
||||
|
||||
public function create( $data = null ) {
|
||||
}
|
||||
|
||||
public function edit( $data = null ) {
|
||||
}
|
||||
|
||||
public function view( $id = null ) {
|
||||
$data = self::$memberships->findById( $id );
|
||||
if ( $data !== false ) {
|
||||
return Views::view( 'members.admin.memberships.view', $data );
|
||||
}
|
||||
Issues::add( 'error', 'Membership not found.' );
|
||||
$this->index();
|
||||
}
|
||||
|
||||
public function delete( $data = null ) {
|
||||
}
|
||||
|
||||
public function preview( $data = null ) {
|
||||
}
|
||||
}
|
188
app/plugins/members/controllers/api/stripe.php
Normal file
188
app/plugins/members/controllers/api/stripe.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
/**
|
||||
* app/controllers/api/users.php
|
||||
*
|
||||
* This is the users' api controller.
|
||||
*
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Controllers\Api;
|
||||
|
||||
use Stripe\StripeClient;
|
||||
use Stripe\Event;
|
||||
use TheTempusProject\Models\User;
|
||||
use TheTempusProject\Classes\ApiController;
|
||||
use TheTempusProject\Houdini\Classes\Views;
|
||||
use TheTempusProject\Bedrock\Classes\Config;
|
||||
use TheTempusProject\Canary\Bin\Canary as Debug;
|
||||
use TheTempusProject\Models\MembershipCustomers;
|
||||
use TheTempusProject\Models\Memberships;
|
||||
|
||||
class Stripe extends ApiController {
|
||||
public static $stripe;
|
||||
public static $customers;
|
||||
public static $memberships;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct( false );
|
||||
$api_key = Config::getValue( 'memberships/stripeSecret' );
|
||||
self::$stripe = new StripeClient($api_key);
|
||||
}
|
||||
|
||||
public function webhook() {
|
||||
try {
|
||||
$payload = @file_get_contents('php://input');
|
||||
$payload = json_decode( $payload, true );
|
||||
if ( ! is_array( $payload ) ) {
|
||||
throw new \Exception("Error Processing Request", 1);
|
||||
}
|
||||
$event = null;
|
||||
$event = Event::constructFrom( $payload );
|
||||
|
||||
$eventData = $event->data->object;
|
||||
|
||||
// $event->type gives the event type obv
|
||||
switch ($event->type) {
|
||||
// case 'invoice.paid':
|
||||
// Debug::error( 'processing: ' . $event->type );
|
||||
// "id": "in_1QSDcJGsigymNdIJo0Z1a20K",
|
||||
// "customer": "cus_RKtOtR7X7CwPRU",
|
||||
// "charge": "ch_3QSDcJGsigymNdIJ0Smb7Rmx",
|
||||
// "subscription": "sub_1QSDcJGsigymNdIJWGw7Zrv9",
|
||||
// break;
|
||||
|
||||
// case 'charge.succeeded':
|
||||
// Debug::error( 'processing: ' . $event->type );
|
||||
// "id": "ch_3QSDcJGsigymNdIJ0Smb7Rmx",
|
||||
// "invoice": "in_1QSDcJGsigymNdIJo0Z1a20K",
|
||||
// "status": "succeeded",
|
||||
// break;
|
||||
|
||||
case 'customer.subscription.updated':
|
||||
case 'customer.subscription.paused':
|
||||
case 'customer.subscription.resumed':
|
||||
case 'customer.subscription.deleted':
|
||||
Debug::error( 'processing: ' . $event->type );
|
||||
|
||||
self::$memberships = new Memberships;
|
||||
$membership_id = self::$memberships->findBySubscriptionID( $eventData->id );
|
||||
if ( empty( $membership_id ) ) {
|
||||
Debug::error( 'membership not found' );
|
||||
|
||||
self::$customers = new MembershipCustomers;
|
||||
$customer = self::$customers->findByCustomerID( $eventData->customer );
|
||||
if ( empty( $customer ) ) {
|
||||
Debug::error( 'customer not found' );
|
||||
Debug::v( $eventData->customer );
|
||||
break;
|
||||
}
|
||||
|
||||
$result = self::$memberships->create(
|
||||
$eventData->customer,
|
||||
$eventData->id,
|
||||
$eventData->plan->id,
|
||||
$eventData->current_period_start,
|
||||
$eventData->current_period_end,
|
||||
$eventData->status,
|
||||
$customer->local_user,
|
||||
'frequency'
|
||||
);
|
||||
} else {
|
||||
$result = self::$memberships->update( $membership_id->ID, $eventData->current_period_start, $eventData->current_period_end, $eventData->status );
|
||||
}
|
||||
|
||||
if ( empty( $result ) ) {
|
||||
Debug::error( 'membership not updated' );
|
||||
Debug::v( $result );
|
||||
}
|
||||
break;
|
||||
case 'customer.subscription.created':
|
||||
Debug::error( 'processing: ' . $event->type );
|
||||
|
||||
self::$memberships = new Memberships;
|
||||
$membership_id = self::$memberships->findBySubscriptionID( $eventData->id );
|
||||
if ( ! empty( $membership_id ) ) {
|
||||
Debug::error( 'subscription already created' );
|
||||
break;
|
||||
}
|
||||
self::$customers = new MembershipCustomers;
|
||||
$customer = self::$customers->findByCustomerID( $eventData->customer );
|
||||
if ( empty( $customer ) ) {
|
||||
Debug::error( 'customer not found' );
|
||||
Debug::v( $eventData->customer );
|
||||
break;
|
||||
}
|
||||
Debug::error( 'processing: ' . $event->type );
|
||||
|
||||
$result = self::$memberships->create(
|
||||
$eventData->customer,
|
||||
$eventData->id,
|
||||
$eventData->plan->id,
|
||||
$eventData->current_period_start,
|
||||
$eventData->current_period_end,
|
||||
$eventData->status,
|
||||
$customer->local_user,
|
||||
'frequency'
|
||||
);
|
||||
Debug::error( 'processing: ' . $event->type );
|
||||
|
||||
if ( empty( $result ) ) {
|
||||
Debug::error( 'membership not made' );
|
||||
Debug::v( $result );
|
||||
}
|
||||
Debug::error( 'done processing: ' . var_export($result,true) );
|
||||
break;
|
||||
// case 'invoice.created':
|
||||
// Debug::error( 'processing: ' . $event->type );
|
||||
// happens when the payment first starts
|
||||
|
||||
// "id": "in_1QSDcJGsigymNdIJo0Z1a20K",
|
||||
// "customer": "cus_RKtOtR7X7CwPRU",
|
||||
// "status": "open",
|
||||
// "total": 888,
|
||||
// break;
|
||||
// case 'checkout.session.completed':
|
||||
// Debug::error( 'processing: ' . $event->type );
|
||||
// new customer has completed first checkout
|
||||
// add thier record or update iit
|
||||
|
||||
// "invoice": "in_1QSDcJGsigymNdIJo0Z1a20K",
|
||||
// "mode": "subscription",
|
||||
// "status": "complete",
|
||||
// "customer": "cus_RKtOtR7X7CwPRU",
|
||||
// break;
|
||||
// case 'payment_intent.succeeded':
|
||||
// Debug::error( 'processing: ' . $event->type );
|
||||
// $paymentIntent = $event->data->object;
|
||||
// break;
|
||||
// case 'payment_method.attached':
|
||||
// Debug::error( 'processing: ' . $event->type );
|
||||
// $paymentMethod = $event->data->object;
|
||||
// break;
|
||||
default:
|
||||
Debug::error( 'Skipped Event:' . $event->type );
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
$responseType = 'success';
|
||||
$response = true;
|
||||
} catch(\UnexpectedValueException $e) {
|
||||
Debug::error( 'UnexpectedValueException' );
|
||||
Debug::v( $e );
|
||||
http_response_code(400);
|
||||
$responseType = 'error';
|
||||
$response = 'UnexpectedValueException';
|
||||
} catch(\Exception $e) {
|
||||
Debug::error( 'Exception' );
|
||||
Debug::error( $e );
|
||||
http_response_code(400);
|
||||
$responseType = 'error';
|
||||
$response = 'Exception';
|
||||
}
|
||||
Views::view( 'api.response', ['response' => json_encode( [ $responseType => $response ], true )]);
|
||||
}
|
||||
}
|
339
app/plugins/members/controllers/member.php
Normal file
339
app/plugins/members/controllers/member.php
Normal file
@ -0,0 +1,339 @@
|
||||
<?php
|
||||
/**
|
||||
* app/controllers/member.php
|
||||
*
|
||||
* This is the members controller.
|
||||
*
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Controllers;
|
||||
|
||||
use TheTempusProject\Houdini\Classes\Template;
|
||||
use TheTempusProject\Houdini\Classes\Views;
|
||||
use TheTempusProject\Houdini\Classes\Issues;
|
||||
use TheTempusProject\Classes\Controller;
|
||||
use TheTempusProject\TheTempusProject as App;
|
||||
use TheTempusProject\Hermes\Functions\Redirect;
|
||||
use TheTempusProject\Bedrock\Functions\Session;
|
||||
use TheTempusProject\Models\MembershipCustomers;
|
||||
use TheTempusProject\Models\MembershipProducts;
|
||||
use TheTempusProject\Bedrock\Classes\Config;
|
||||
use TheTempusProject\Bedrock\Functions\Input;
|
||||
use Stripe\Checkout\Session as StripeSession;
|
||||
use TheTempusProject\Hermes\Functions\Route as Routes;
|
||||
use TheTempusProject\Houdini\Classes\Navigation;
|
||||
use TheTempusProject\Models\Memberships;
|
||||
use TheTempusProject\Houdini\Classes\Components;
|
||||
use TheTempusProject\Classes\Forms;
|
||||
use TheTempusProject\Bedrock\Functions\Hash;
|
||||
use TheTempusProject\Canary\Bin\Canary as Debug;
|
||||
use Stripe\StripeClient;
|
||||
|
||||
class Member extends Controller {
|
||||
public static $customers;
|
||||
public static $products;
|
||||
public static $stripe;
|
||||
private static $loaded = false;
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
|
||||
if ( ! self::$loaded ) {
|
||||
Template::noIndex();
|
||||
self::$customers = new MembershipCustomers;
|
||||
self::$products = new MembershipProducts;
|
||||
$api_key = Config::getValue( 'memberships/stripeSecret' );
|
||||
if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) {
|
||||
Debug::error( "Memberships:__construct No Stripe Key found" );
|
||||
} else {
|
||||
self::$stripe = new StripeClient( $api_key );
|
||||
}
|
||||
self::$loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function index() {
|
||||
$this->confirmAuth();
|
||||
self::$title = 'Members Area';
|
||||
Views::view( 'members.members' );
|
||||
}
|
||||
|
||||
public function managepayment() {
|
||||
$this->confirmAuth();
|
||||
$customer = self::$customers->findByUserID( App::$activeUser->ID );
|
||||
if ( empty( $customer ) ) {
|
||||
Session::flash( 'error', 'You do not have any active payment methods. You can subscribe by going <a href="/member/join/1">here</a>' );
|
||||
return Redirect::to( 'member/manage' );
|
||||
}
|
||||
try {
|
||||
$session = self::$stripe->billingPortal->sessions->create([
|
||||
'customer' => $customer->stripe_customer,
|
||||
'return_url' => Routes::getAddress() . 'member/manage',
|
||||
]);
|
||||
} catch (\Stripe\Exception\InvalidRequestException $e) {
|
||||
Debug::error('Membership -> ManagePayment - Stripe not configured correctly');
|
||||
Debug::error( $e );
|
||||
Session::flash( 'error', 'There was an issue redirecting you to Stripe, please try again.' );
|
||||
return Redirect::to( 'member/manage' );
|
||||
}
|
||||
|
||||
header('Location: ' . $session->url);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function cancelconfirm( $id = null ) {
|
||||
$this->confirmAuth( $id );
|
||||
$memberships = new Memberships;
|
||||
$result = $memberships->cancel( $id );
|
||||
if ( ! empty( $result ) ) {
|
||||
Session::flash( 'success', 'Your Membership has been paused.' );
|
||||
Redirect::to( 'member/manage' );
|
||||
} else {
|
||||
Session::flash( 'error', 'There was an error canceling your membership' );
|
||||
Redirect::to( 'member/manage' );
|
||||
}
|
||||
}
|
||||
|
||||
public function pauseconfirm( $id = null ) {
|
||||
$this->confirmAuth( $id );
|
||||
$memberships = new Memberships;
|
||||
$result = $memberships->cancel( $id );
|
||||
if ( ! empty( $result ) ) {
|
||||
Session::flash( 'success', 'Your Membership has been paused.' );
|
||||
Redirect::to( 'member/manage' );
|
||||
} else {
|
||||
Session::flash( 'error', 'There was an error canceling your membership' );
|
||||
Redirect::to( 'member/manage' );
|
||||
}
|
||||
}
|
||||
|
||||
public function pause( $id = null ) {
|
||||
$this->confirmAuth( $id );
|
||||
self::$title = 'pause Membership';
|
||||
Components::set( 'pauseid', $id );
|
||||
Views::view( 'members.pause' );
|
||||
}
|
||||
|
||||
public function resume( $id = null ) {
|
||||
$this->confirmAuth( $id );
|
||||
self::$title = 'resume Membership';
|
||||
Views::view( 'members.resume' );
|
||||
}
|
||||
|
||||
public function cancel( $id = null ) {
|
||||
$this->confirmAuth( $id );
|
||||
self::$title = 'Cancel Membership';
|
||||
Components::set( 'cancelid', $id );
|
||||
Views::view( 'members.cancel' );
|
||||
}
|
||||
|
||||
public function manage() {
|
||||
if ( ! App::$isLoggedIn ) {
|
||||
Session::flash( 'error', 'You do not have permission to access this page.' );
|
||||
return Redirect::home();
|
||||
}
|
||||
self::$title = 'Manage Membership';
|
||||
|
||||
$menu = Views::simpleView( 'nav.usercp', App::$userCPlinks );
|
||||
Navigation::activePageSelect( $menu, null, true, true );
|
||||
|
||||
$memberships = new Memberships;
|
||||
$userMemberships = $memberships->getUserSubs();
|
||||
Views::view( 'members.manage', $userMemberships );
|
||||
}
|
||||
|
||||
public function upgrade() {
|
||||
if ( ! App::$isLoggedIn ) {
|
||||
Session::flash( 'error', 'You do not have permission to access this page.' );
|
||||
return Redirect::home();
|
||||
}
|
||||
// need to check if the plan CAN be upgraded
|
||||
self::$title = 'Upgrade Membership';
|
||||
Views::view( 'members.upgrade' );
|
||||
}
|
||||
|
||||
public function join( $plan = 'monthly' ) {
|
||||
if ( ! App::$isLoggedIn ) {
|
||||
Session::flash( 'error', 'You do not have permission to access this page.' );
|
||||
return Redirect::home();
|
||||
}
|
||||
$plan = strtolower( $plan );
|
||||
if ( ! in_array( $plan, ['monthly','yearly'] ) ) {
|
||||
Session::flash( 'error', 'Unknown plan' );
|
||||
return Redirect::to( 'home/index' );
|
||||
}
|
||||
|
||||
self::$title = 'Join {SITENAME}!';
|
||||
$stripePrice = $this->findPrice( $plan );
|
||||
|
||||
$product = self::$products->findByPriceID( $stripePrice );
|
||||
if ( empty( $product ) ) {
|
||||
Session::flash( 'success', 'We aren\'t currently accepting new members, please check back soon!' );
|
||||
return Redirect::home();
|
||||
}
|
||||
Views::view( 'members.join', $product );
|
||||
}
|
||||
|
||||
public function checkout( $plan = 'monthly' ) {
|
||||
if ( ! App::$isLoggedIn ) {
|
||||
Session::flash( 'error', 'You do not have permission to access this page.' );
|
||||
return Redirect::home();
|
||||
}
|
||||
$customer = self::$customers->findOrCreate( App::$activeUser->ID );
|
||||
if ( empty( $customer ) ) {
|
||||
Issues::add( 'error', 'no customer' );
|
||||
return $this->index();
|
||||
}
|
||||
|
||||
$plan = strtolower( $plan );
|
||||
if ( ! in_array( $plan, ['monthly','yearly','upgrade'] ) ) {
|
||||
Session::flash( 'error', 'Unknown plan' );
|
||||
return Redirect::to( 'home/index' );
|
||||
}
|
||||
if ( $plan === 'upgrade' ) {
|
||||
$plan = 'yearly';
|
||||
$successUrl = Routes::getAddress() . 'member/payment/upgrade?session_id={CHECKOUT_SESSION_ID}';
|
||||
} else {
|
||||
$successUrl = Routes::getAddress() . 'member/payment/complete?session_id={CHECKOUT_SESSION_ID}';
|
||||
}
|
||||
|
||||
$stripePrice = $this->findPrice( $plan );
|
||||
|
||||
$session = self::$stripe->checkout->sessions->create([
|
||||
'payment_method_types' => ['card'],
|
||||
'customer' => $customer,
|
||||
'line_items' => [[
|
||||
'price' => $stripePrice,
|
||||
'quantity' => 1,
|
||||
]],
|
||||
'mode' => 'subscription',
|
||||
'allow_promotion_codes' => true,
|
||||
'success_url' => $successUrl,
|
||||
'cancel_url' => Routes::getAddress() . 'member/payment/cancel',
|
||||
]);
|
||||
header('Location: ' . $session->url);
|
||||
exit;
|
||||
}
|
||||
|
||||
public function payment( $type = '' ) {
|
||||
$type = strtolower( $type );
|
||||
if ( ! in_array( $type, ['cancel','complete','upgrade'] ) ) {
|
||||
Session::flash( 'error', 'Unknown Payment' );
|
||||
return Redirect::to( 'home/index' );
|
||||
}
|
||||
|
||||
if ( $type == 'cancel' ) {
|
||||
self::$title = '(almost) Members Area';
|
||||
return Views::view( 'members.paymentcanceled' );
|
||||
}
|
||||
|
||||
if ( $type == 'upgrade' ) {
|
||||
self::$title = 'Better Members Area';
|
||||
return Views::view( 'members.upgradeCompleted' );
|
||||
}
|
||||
|
||||
self::$title = '(almost) Members Area';
|
||||
Views::view( 'members.paymentcomplete' );
|
||||
}
|
||||
|
||||
// This combines a registration with a checkout
|
||||
public function signup( $plan = 'monthly' ) {
|
||||
$plan = strtolower( $plan );
|
||||
if ( ! in_array( $plan, ['monthly','yearly'] ) ) {
|
||||
Session::flash( 'error', 'Unknown plan' );
|
||||
return Redirect::to( 'home/index' );
|
||||
}
|
||||
|
||||
$product = self::$products->mainProduct();
|
||||
if ( empty( $product ) ) {
|
||||
Session::flash( 'error', 'Unknown product' );
|
||||
return Redirect::to( 'home/index' );
|
||||
}
|
||||
|
||||
$stripePrice = $this->findPrice( $plan );
|
||||
|
||||
$pretty = 'prettyPrice' . ucfirst( $plan );
|
||||
$prettyPrice = $product->$pretty;
|
||||
|
||||
self::$title = 'Sign up for {SITENAME} ' . ucfirst( $plan );
|
||||
|
||||
Components::set( 'planName', ucfirst( $plan ) );
|
||||
Components::set( 'prettyPrice', $prettyPrice );
|
||||
Components::set( 'TERMS', Views::simpleView( 'terms' ) );
|
||||
|
||||
if ( App::$isLoggedIn ) {
|
||||
Session::flash( 'notice', 'You are already logged in, you can <a href="/member/join">subscribe here</a>, or <a href="/member/upgrade">upgrade here</a>.' );
|
||||
return Redirect::to( 'home/index' );
|
||||
}
|
||||
|
||||
if ( !Input::exists() ) {
|
||||
return Views::view( 'members.register' );
|
||||
}
|
||||
|
||||
if ( ! Forms::check( 'register' ) ) {
|
||||
Issues::add( 'error', [ 'There was an error with your registration.' => Check::userErrors() ] );
|
||||
return Views::view( 'members.register' );
|
||||
}
|
||||
|
||||
self::$user->create( [
|
||||
'username' => Input::post( 'username' ),
|
||||
'password' => Hash::make( Input::post( 'password' ) ),
|
||||
'email' => Input::post( 'email' ),
|
||||
'terms' => 1,
|
||||
] );
|
||||
|
||||
if ( !self::$user->logIn( Input::post( 'username' ), Input::post( 'password' ), Input::post( 'remember' ) ) ) {
|
||||
Session::flash( 'error', 'Thank you for registering! Unfortunately, there was an issue logging you in, please log in and order again.' );
|
||||
return Redirect::to( 'home/index' );
|
||||
}
|
||||
|
||||
$user = self::$user->authorize( Input::post( 'username' ), Input::post( 'password' ) );
|
||||
|
||||
$customer = self::$customers->findOrCreate( $user->ID );
|
||||
if ( empty( $customer ) ) {
|
||||
Session::flash( 'error', 'Thank you for registering! Unfortunately, there was an issue communicating with Stripe and we can\'t collect payment right now.' );
|
||||
return Redirect::to( 'home/index' );
|
||||
}
|
||||
$session = self::$stripe->checkout->sessions->create([
|
||||
'payment_method_types' => ['card'],
|
||||
'customer' => $customer,
|
||||
'line_items' => [[
|
||||
'price' => $stripePrice,
|
||||
'quantity' => 1,
|
||||
]],
|
||||
'mode' => 'subscription',
|
||||
'allow_promotion_codes' => true,
|
||||
'success_url' => Routes::getAddress() . 'member/payment/complete?session_id={CHECKOUT_SESSION_ID}',
|
||||
'cancel_url' => Routes::getAddress() . 'member/payment/cancel',
|
||||
]);
|
||||
header('Location: ' . $session->url);
|
||||
exit;
|
||||
}
|
||||
|
||||
private function findPrice( $plan ) {
|
||||
$plan = strtolower( $plan );
|
||||
if ( ! in_array( $plan, ['monthly','yearly'] ) ) {
|
||||
Session::flash( 'error', 'Unknown plan' );
|
||||
return Redirect::to( 'home/index' );
|
||||
}
|
||||
|
||||
$product = self::$products->mainProduct();
|
||||
if ( empty( $product ) ) {
|
||||
Session::flash( 'error', 'Unknown product' );
|
||||
return Redirect::to( 'home/index' );
|
||||
}
|
||||
$index = 'stripe_price_' . $plan;
|
||||
$stripePrice = $product->$index;
|
||||
return $stripePrice;
|
||||
}
|
||||
|
||||
private function confirmAuth() {
|
||||
if ( ! App::$isLoggedIn || ! App::$isMember ) {
|
||||
Session::flash( 'error', 'You do not have permission to access this page.' );
|
||||
return Redirect::home();
|
||||
}
|
||||
}
|
||||
}
|
47
app/plugins/members/forms.php
Normal file
47
app/plugins/members/forms.php
Normal file
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* app/plugins/subscribe/forms.php
|
||||
*
|
||||
* This houses all of the form checking functions for this plugin.
|
||||
*
|
||||
* @package TP Subscribe
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Plugins\Subscribe;
|
||||
|
||||
use TheTempusProject\Bedrock\Functions\Input;
|
||||
use TheTempusProject\Classes\Forms;
|
||||
|
||||
class MembershipForms extends Forms {
|
||||
/**
|
||||
* Adds these functions to the form list.
|
||||
*/
|
||||
public function __construct() {
|
||||
self::addHandler( 'newMembershipProduct', __CLASS__, 'newMembershipProduct' );
|
||||
self::addHandler( 'editMembershipProduct', __CLASS__, 'editMembershipProduct' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the subscribe form.
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
public static function newMembershipProduct() {
|
||||
// if ( !self::token() ) {
|
||||
// return false;
|
||||
// }
|
||||
return true;
|
||||
}
|
||||
public static function editMembershipProduct() {
|
||||
// if ( !self::token() ) {
|
||||
// return false;
|
||||
// }
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
new MembershipForms;
|
104
app/plugins/members/models/membership_customers.php
Normal file
104
app/plugins/members/models/membership_customers.php
Normal file
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
/**
|
||||
* app/plugins/members/models/membership_customers.php
|
||||
*
|
||||
* This class is used for the manipulation of the membership_customers database table.
|
||||
*
|
||||
* @package TP Members
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Models;
|
||||
|
||||
use TheTempusProject\Canary\Bin\Canary as Debug;
|
||||
use TheTempusProject\Bedrock\Functions\Check;
|
||||
use TheTempusProject\Bedrock\Functions\Sanitize;
|
||||
use TheTempusProject\Classes\DatabaseModel;
|
||||
use TheTempusProject\TheTempusProject as App;
|
||||
use TheTempusProject\Bedrock\Classes\Config;
|
||||
use TheTempusProject\Hermes\Functions\Route as Routes;
|
||||
|
||||
class MembershipCustomers extends DatabaseModel {
|
||||
public static $stripe;
|
||||
public $tableName = 'membership_customers';
|
||||
|
||||
public $databaseMatrix = [
|
||||
[ 'stripe_customer', 'varchar', '155' ],
|
||||
[ 'local_user', 'varchar', '155' ],
|
||||
// renews?
|
||||
// does this renew periodically?
|
||||
// renewal period?
|
||||
// if periodic, how frequent?
|
||||
// renewal type?
|
||||
// automatic, manual review, paid
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function findByUserID( $d ) {
|
||||
$data = self::$db->get( $this->tableName, [ 'local_user', '=', $d ] );
|
||||
if ( ! $data->count() ) {
|
||||
return false;
|
||||
}
|
||||
return $data->first();
|
||||
}
|
||||
|
||||
public function findByCustomerID( $d ) {
|
||||
$data = self::$db->get( $this->tableName, [ 'stripe_customer', '=', $d ] );
|
||||
if ( ! $data->count() ) {
|
||||
return false;
|
||||
}
|
||||
return $data->first();
|
||||
}
|
||||
|
||||
public function create( $user_id ) {
|
||||
$data = self::$db->get( 'users', ['ID', '=', $user_id] );
|
||||
if ( !$data->count() ) {
|
||||
Debug::warn( "customer cannot be created, user not found" );
|
||||
return false;
|
||||
}
|
||||
$user = $data->first();
|
||||
$user_email = $user->email;
|
||||
$user_name = $user->name;
|
||||
|
||||
$api_key = Config::getValue( 'memberships/stripeSecret' );
|
||||
if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) {
|
||||
Debug::error( "MembershipCustomers:create No Stripe Key found" );
|
||||
return false;
|
||||
}
|
||||
self::$stripe = new \Stripe\StripeClient( $api_key );
|
||||
|
||||
$customer = self::$stripe->customers->create([
|
||||
'name' => $user_name,
|
||||
'email' => $user_email,
|
||||
]);
|
||||
|
||||
$fields = [
|
||||
'stripe_customer' => $customer->id,
|
||||
'local_user' => $user_id,
|
||||
];
|
||||
|
||||
if ( !self::$db->insert( $this->tableName, $fields ) ) {
|
||||
Debug::error( "Membership Customer: $data not added: $fields" );
|
||||
new customException( 'membershipCustomerCreate' );
|
||||
return false;
|
||||
}
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
public function findOrCreate( $user_id ) {
|
||||
$user = $this->findByUserID( $user_id );
|
||||
if ( ! empty( $user ) ) {
|
||||
return $user->stripe_customer;
|
||||
}
|
||||
$user = $this->create( $user_id );
|
||||
if ( ! empty( $user ) ) {
|
||||
return $user->id;
|
||||
}
|
||||
}
|
||||
}
|
38
app/plugins/members/models/membership_invoices.php
Normal file
38
app/plugins/members/models/membership_invoices.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* app/plugins/members/models/membership_invoices.php
|
||||
*
|
||||
* This class is used for the manipulation of the membership_invoices database table.
|
||||
*
|
||||
* @package TP Members
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Models;
|
||||
|
||||
use TheTempusProject\Canary\Bin\Canary as Debug;
|
||||
use TheTempusProject\Bedrock\Functions\Check;
|
||||
use TheTempusProject\Bedrock\Functions\Sanitize;
|
||||
use TheTempusProject\Classes\DatabaseModel;
|
||||
use TheTempusProject\TheTempusProject as App;
|
||||
|
||||
class MembershipInvoices extends DatabaseModel {
|
||||
public static $stripe;
|
||||
public $tableName = 'membership_invoices';
|
||||
|
||||
public $databaseMatrix = [
|
||||
[ 'name', 'varchar', '155' ],
|
||||
// renews?
|
||||
// does this renew periodically?
|
||||
// renewal period?
|
||||
// if periodic, how frequent?
|
||||
// renewal type?
|
||||
// automatic, manual review, paid
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
}
|
||||
}
|
222
app/plugins/members/models/membership_products.php
Normal file
222
app/plugins/members/models/membership_products.php
Normal file
@ -0,0 +1,222 @@
|
||||
<?php
|
||||
/**
|
||||
* app/plugins/members/models/membership_products.php
|
||||
*
|
||||
* This class is used for the manipulation of the membership_products database table.
|
||||
*
|
||||
* @package TP Members
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
|
||||
namespace TheTempusProject\Models;
|
||||
|
||||
use TheTempusProject\Canary\Bin\Canary as Debug;
|
||||
use TheTempusProject\Bedrock\Functions\Check;
|
||||
use TheTempusProject\Bedrock\Functions\Sanitize;
|
||||
use TheTempusProject\Classes\DatabaseModel;
|
||||
use TheTempusProject\TheTempusProject as App;
|
||||
use TheTempusProject\Bedrock\Classes\Config;
|
||||
|
||||
class MembershipProducts extends DatabaseModel {
|
||||
public static $stripe;
|
||||
private static $loaded = false;
|
||||
public $tableName = 'membership_products';
|
||||
public $databaseMatrix = [
|
||||
[ 'name', 'varchar', '128' ],
|
||||
[ 'description', 'text', '' ],
|
||||
[ 'monthly_price', 'int', '10' ], // must be int value greater than 99 IE $1.00 === 100, $4,399.22 === 439922
|
||||
[ 'yearly_price', 'int', '10' ], // must be int value greater than 99 IE $1.00 === 100, $4,399.22 === 439922
|
||||
[ 'stripe_product', 'varchar', '64' ], // not-required to create - generated by stripe after
|
||||
[ 'stripe_price_monthly', 'varchar', '64' ], // not-required to create - generated by stripe after
|
||||
[ 'stripe_price_yearly', 'varchar', '64' ], // not-required to create - generated by stripe after
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
if ( ! self::$loaded ) {
|
||||
$api_key = Config::getValue( 'memberships/stripeSecret' );
|
||||
if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) {
|
||||
Debug::error( "MembershipProducts:__construct No Stripe Key found" );
|
||||
} else {
|
||||
self::$stripe = new \Stripe\StripeClient( $api_key );
|
||||
}
|
||||
self::$loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function create( $name, $description, $monthly_price, $yearly_price ) {
|
||||
if ( empty( self::$stripe ) ) {
|
||||
return false;
|
||||
}
|
||||
$stripe_product = $this->createStripeProduct( $name, $description );
|
||||
$stripe_prices = $this->createStripePrices( $stripe_product->id, $monthly_price, $yearly_price );
|
||||
|
||||
$fields = [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'monthly_price' => $monthly_price,
|
||||
'yearly_price' => $yearly_price,
|
||||
'stripe_product' => $stripe_product->id,
|
||||
'stripe_price_monthly' => $stripe_prices['monthly']->id,
|
||||
'stripe_price_yearly' => $stripe_prices['yearly']->id,
|
||||
];
|
||||
|
||||
if ( !self::$db->insert( $this->tableName, $fields ) ) {
|
||||
Debug::error( "Membership Product: $data not updated: $fields" );
|
||||
new customException( 'membershipProductCreate' );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function createStripeProduct( $name, $description ) {
|
||||
if ( empty( self::$stripe ) ) {
|
||||
return false;
|
||||
}
|
||||
$product = self::$stripe->products->create([
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
]);
|
||||
return $product;
|
||||
}
|
||||
|
||||
public function createStripePrices( $product, $monthly_price, $yearly_price ) {
|
||||
$out = [];
|
||||
$out['monthly'] = $this->createStripeMonthlyPrice( $product, $monthly_price );
|
||||
$out['yearly'] = $this->createStripeYearlyPrice( $product, $yearly_price );
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function createStripeMonthlyPrice( $product, $monthly_price ) {
|
||||
if ( empty( self::$stripe ) ) {
|
||||
return false;
|
||||
}
|
||||
return self::$stripe->prices->create([
|
||||
'currency' => 'usd',
|
||||
'unit_amount' => $monthly_price,
|
||||
'recurring' => ['interval' => 'month'],
|
||||
'product' => $product,
|
||||
'lookup_key' => 'membership-monthly',
|
||||
]);
|
||||
}
|
||||
|
||||
public function createStripeYearlyPrice( $product, $yearly_price ) {
|
||||
if ( empty( self::$stripe ) ) {
|
||||
return false;
|
||||
}
|
||||
return self::$stripe->prices->create([
|
||||
'currency' => 'usd',
|
||||
'unit_amount' => $yearly_price,
|
||||
'recurring' => ['interval' => 'year'],
|
||||
'product' => $product,
|
||||
'lookup_key' => 'membership-yearly',
|
||||
]);
|
||||
}
|
||||
|
||||
public function updateProduct( $id, $name, $description, $monthly_price, $yearly_price ) {
|
||||
$product = $this->findById( $id );
|
||||
if ( $product === false ) {
|
||||
return false;
|
||||
}
|
||||
if ( $product->monthly_price != $monthly_price ) {
|
||||
$new_monthly = $this->updateStripeMonthlyPrice( $product->stripe_price_monthly, $monthly_price );
|
||||
}
|
||||
if ( $product->yearly_price != $yearly_price ) {
|
||||
$new_yearly = $this->updateStripeYearlyPrice( $product->stripe_price_yearly, $yearly_price );
|
||||
}
|
||||
if ( ( $product->name != $name ) || ( $product->description != $description ) ) {
|
||||
$this->updateStripeProduct( $product->stripe_product, $name, $description );
|
||||
}
|
||||
|
||||
$fields = [
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'monthly_price' => $monthly_price,
|
||||
'yearly_price' => $yearly_price,
|
||||
'stripe_price_monthly' => $new_monthly->id,
|
||||
'stripe_price_yearly' => $new_yearly->id,
|
||||
];
|
||||
if ( !self::$db->update( $this->tableName, $id, $fields ) ) {
|
||||
new CustomException( 'membershipProductUpdate' );
|
||||
Debug::error( "membership Product: $id not updated: $fields" );
|
||||
|
||||
return false;
|
||||
}
|
||||
self::$log->admin( "Updated membership Product: $id" );
|
||||
return true;
|
||||
}
|
||||
|
||||
public function updateStripeProduct( $id, $name, $description ) {
|
||||
if ( empty( self::$stripe ) ) {
|
||||
return false;
|
||||
}
|
||||
$product = self::$stripe->products->update(
|
||||
$id,
|
||||
[
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
]
|
||||
);
|
||||
return $product;
|
||||
}
|
||||
|
||||
public function updateStripeYearlyPrice( $product, $yearly_price ) {
|
||||
$stripe->prices->update(
|
||||
$yearly_price,
|
||||
['lookup_key' => ""]
|
||||
);
|
||||
return $this->createStripeYearlyPrice( $product, $yearly_price );
|
||||
}
|
||||
|
||||
public function updateStripeMonthlyPrice( $product, $monthly_price ) {
|
||||
$stripe->prices->update(
|
||||
$monthly_price,
|
||||
['lookup_key' => ""]
|
||||
);
|
||||
return $this->createStripeMonthlyPrice( $product, $monthly_price );
|
||||
}
|
||||
|
||||
public function filter( $postArray, $params = [] ) {
|
||||
foreach ( $postArray as $instance ) {
|
||||
if ( !is_object( $instance ) ) {
|
||||
$instance = $postArray;
|
||||
$end = true;
|
||||
}
|
||||
|
||||
$instance->prettyPriceMonthly = '$' . number_format( $instance->monthly_price / 100, 2 ); // Outputs: $99.49
|
||||
$instance->prettyPriceYearly = '$' . number_format( $instance->yearly_price / 100, 2 ); // Outputs: $99.49
|
||||
$instance->prettySavings = '$' . number_format(
|
||||
( $instance->yearly_price - ( $instance->monthly_price * 12 ) ) / 100,
|
||||
2
|
||||
); // Outputs: $99.49
|
||||
|
||||
$out[] = $instance;
|
||||
if ( !empty( $end ) ) {
|
||||
$out = $out[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function findByPriceID( $d ) {
|
||||
$data = self::$db->get( $this->tableName, [ 'stripe_price_monthly', '=', $d, 'OR', 'stripe_price_yearly', '=', $d ] );
|
||||
if ( ! $data->count() ) {
|
||||
return false;
|
||||
}
|
||||
return $this->filter( $data->first() );
|
||||
}
|
||||
|
||||
public function mainProduct() {
|
||||
$data = self::$db->get( $this->tableName, '*' );
|
||||
if ( ! $data->count() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->filter( $data->first() );
|
||||
}
|
||||
}
|
174
app/plugins/members/models/memberships.php
Normal file
174
app/plugins/members/models/memberships.php
Normal file
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
/**
|
||||
* app/plugins/members/models/membership_records.php
|
||||
*
|
||||
* This class is used for the manipulation of the membership_records database table.
|
||||
*
|
||||
* @package TP Members
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Models;
|
||||
|
||||
use TheTempusProject\Canary\Bin\Canary as Debug;
|
||||
use TheTempusProject\Bedrock\Functions\Check;
|
||||
use TheTempusProject\Bedrock\Functions\Sanitize;
|
||||
use TheTempusProject\Classes\DatabaseModel;
|
||||
use TheTempusProject\TheTempusProject as App;
|
||||
use TheTempusProject\Models\MembershipProducts;
|
||||
use TheTempusProject\Bedrock\Classes\Config;
|
||||
use TheTempusProject\Canary\Classes\CustomException;
|
||||
|
||||
class Memberships extends DatabaseModel {
|
||||
public static $stripe;
|
||||
public static $products;
|
||||
private static $loaded = false;
|
||||
public $tableName = 'membership_records';
|
||||
public $databaseMatrix = [
|
||||
[ 'stripe_customer', 'varchar', '64' ],
|
||||
[ 'stripe_subscription', 'varchar', '64' ],
|
||||
[ 'subscription_price_id', 'varchar', '64' ],
|
||||
[ 'current_period_end', 'int', '10' ],
|
||||
[ 'current_period_start', 'int', '10' ],
|
||||
[ 'status', 'varchar', '64' ],
|
||||
[ 'local_user_id', 'int', '10' ],
|
||||
[ 'billing_frequency', 'varchar', '16' ],
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct();
|
||||
if ( ! self::$loaded ) {
|
||||
self::$products = new MembershipProducts;
|
||||
$api_key = Config::getValue( 'memberships/stripeSecret' );
|
||||
if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) {
|
||||
Debug::error( "Memberships:__construct No Stripe Key found" );
|
||||
} else {
|
||||
self::$stripe = new \Stripe\StripeClient( $api_key );
|
||||
}
|
||||
self::$loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function filter( $postArray, $params = [] ) {
|
||||
foreach ( $postArray as $instance ) {
|
||||
if ( !is_object( $instance ) ) {
|
||||
$instance = $postArray;
|
||||
$end = true;
|
||||
}
|
||||
$instance->name = self::$user->getUsername( $instance->local_user_id );
|
||||
$priceData = self::$products->findByPriceID( $instance->subscription_price_id );
|
||||
if ( $priceData ) {
|
||||
if ( $priceData->stripe_price_monthly == $instance->subscription_price_id ) {
|
||||
$price = $priceData->monthly_price;
|
||||
} else {
|
||||
$price = $priceData->yearly_price;
|
||||
}
|
||||
$instance->productName = $priceData->name;
|
||||
$instance->prettyPrice = '$' . number_format( $price / 100, 2 ); // Outputs: $99.49
|
||||
} else {
|
||||
$instance->prettyPrice = "unknown";
|
||||
$instance->productName = "unknown";
|
||||
}
|
||||
$out[] = $instance;
|
||||
if ( !empty( $end ) ) {
|
||||
$out = $out[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function getUserSubs( $limit = null ) {
|
||||
$whereClause = ['local_user_id', '=', App::$activeUser->ID ];
|
||||
if ( empty( $limit ) ) {
|
||||
$postData = self::$db->get( $this->tableName, $whereClause );
|
||||
} else {
|
||||
$postData = self::$db->get( $this->tableName, $whereClause, 'ID', 'DESC', [0, $limit] );
|
||||
}
|
||||
if ( !$postData->count() ) {
|
||||
Debug::info( 'No user subs found.' );
|
||||
return false;
|
||||
}
|
||||
return $this->filter( $postData->results() );
|
||||
}
|
||||
|
||||
public function findByUserID( $d ) {
|
||||
$data = self::$db->get( $this->tableName, [ 'local_user_id', '=', $d ] );
|
||||
if ( ! $data->count() ) {
|
||||
return false;
|
||||
}
|
||||
return $data->first();
|
||||
}
|
||||
|
||||
public function findActiveByUserID( $d ) {
|
||||
$data = self::$db->get( $this->tableName, [ 'local_user_id', '=', $d, 'AND', 'status', '=', 'active' ] );
|
||||
if ( ! $data->count() ) {
|
||||
return false;
|
||||
}
|
||||
return $data->first();
|
||||
}
|
||||
|
||||
public function cancel( $id ) {
|
||||
$data = self::$db->get( $this->tableName, [ 'ID', '=', $id, 'AND', 'local_user_id', '=', App::$activeUser->ID, 'AND', 'status', '=', 'active' ] );
|
||||
if ( ! $data->count() ) {
|
||||
return false;
|
||||
}
|
||||
$membershipID = $data->first()->stripe_subscription;
|
||||
$out = false;
|
||||
try {
|
||||
$out = self::$stripe->subscriptions->cancel($membershipID, []);
|
||||
} catch(\Exception $e) {
|
||||
Debug::error( 'Exception' );
|
||||
Debug::v( $e );
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function findBySubscriptionID( $d ) {
|
||||
$data = self::$db->get( $this->tableName, [ 'stripe_subscription', '=', $d ] );
|
||||
if ( ! $data->count() ) {
|
||||
return false;
|
||||
}
|
||||
return $data->first();
|
||||
}
|
||||
|
||||
public function create( $customer, $subscription, $price, $start, $end, $status, $user_id, $frequency ) {
|
||||
$fields = [
|
||||
'stripe_customer' => $customer,
|
||||
'stripe_subscription' => $subscription,
|
||||
'subscription_price_id' => $price,
|
||||
'current_period_end' => $end,
|
||||
'current_period_start' => $start,
|
||||
'status' => $status,
|
||||
'local_user_id' => $user_id,
|
||||
'billing_frequency' => $frequency,
|
||||
];
|
||||
|
||||
if ( !self::$db->insert( $this->tableName, $fields ) ) {
|
||||
Debug::error( "Memberships: not created: $fields" );
|
||||
new CustomException( 'membershipsCreate' );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function update( $id, $start, $end, $status ) {
|
||||
$fields = [
|
||||
'current_period_end' => $end,
|
||||
'current_period_start' => $start,
|
||||
'status' => $status,
|
||||
];
|
||||
|
||||
if ( !self::$db->update( $this->tableName, $id, $fields ) ) {
|
||||
Debug::error( "Memberships: not updated: " );
|
||||
Debug::v( $fields );
|
||||
new CustomException( 'membershipsUpdate' );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
263
app/plugins/members/plugin.php
Normal file
263
app/plugins/members/plugin.php
Normal file
@ -0,0 +1,263 @@
|
||||
<?php
|
||||
/**
|
||||
* app/plugins/members/plugin.php
|
||||
*
|
||||
* This houses all of the main plugin info and functionality.
|
||||
*
|
||||
* @package TP Members
|
||||
* @version 3.0
|
||||
* @author Joey Kimsey <Joey@thetempusproject.com>
|
||||
* @link https://TheTempusProject.com
|
||||
* @license https://opensource.org/licenses/MIT [MIT LICENSE]
|
||||
*/
|
||||
namespace TheTempusProject\Plugins;
|
||||
|
||||
use TheTempusProject\TheTempusProject as App;
|
||||
use TheTempusProject\Classes\Plugin;
|
||||
use Stripe\StripeClient;
|
||||
use TheTempusProject\Bedrock\Classes\Config;
|
||||
use TheTempusProject\Hermes\Functions\Route as Routes;
|
||||
use TheTempusProject\Models\Memberships;
|
||||
use TheTempusProject\Models\MembershipProducts as Products;
|
||||
use TheTempusProject\Canary\Bin\Canary as Debug;
|
||||
|
||||
class Members extends Plugin {
|
||||
public static $stripe;
|
||||
public static $memberships;
|
||||
private static $loaded = false;
|
||||
public $pluginName = 'TP Membership';
|
||||
public $configName = 'memberships';
|
||||
public $pluginAuthor = 'JoeyK';
|
||||
public $pluginWebsite = 'https://TheTempusProject.com';
|
||||
public $modelVersion = '1.0';
|
||||
public $pluginVersion = '3.0';
|
||||
public $pluginDescription = 'A simple plugin which adds a site wide membership system.';
|
||||
public $permissionMatrix = [
|
||||
'memberAccess' => [
|
||||
'pretty' => 'Access Member Areas',
|
||||
'default' => false,
|
||||
],
|
||||
'controlMemberships' => [
|
||||
'pretty' => 'User can Access and Control user memberships.',
|
||||
'default' => false,
|
||||
],
|
||||
];
|
||||
public $admin_links = [
|
||||
[
|
||||
'text' => '<i class="fa fa-fw fa-arrow-down"></i> Memberships',
|
||||
'url' => [
|
||||
[
|
||||
'text' => '<i class="fa fa-fw fa-barcode"></i> Products',
|
||||
'url' => '{ROOT_URL}admin/products',
|
||||
],
|
||||
[
|
||||
'text' => '<i class="fa fa-fw fa-bag-shopping"></i> Subscriptions',
|
||||
'url' => '{ROOT_URL}admin/records',
|
||||
],
|
||||
[
|
||||
'text' => '<i class="fa fa-fw fa-code"></i> Scripts',
|
||||
'url' => '{ROOT_URL}admin/members',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
public $main_links = [
|
||||
[
|
||||
'text' => 'My Membership',
|
||||
'url' => '{ROOT_URL}member/index',
|
||||
'filter' => 'member',
|
||||
],
|
||||
[
|
||||
'text' => 'Subscribe',
|
||||
'url' => '{ROOT_URL}member/join',
|
||||
'filter' => 'nonmember',
|
||||
],
|
||||
[
|
||||
'text' => 'Upgrade',
|
||||
'url' => '{ROOT_URL}member/upgrade',
|
||||
'filter' => 'upgrade',
|
||||
],
|
||||
];
|
||||
public $resourceMatrix = [
|
||||
'groups' => [
|
||||
[
|
||||
'name' => 'Member',
|
||||
'permissions' => '{"adminAccess":false}',
|
||||
]
|
||||
],
|
||||
];
|
||||
public $configMatrix = [
|
||||
'stripePublishable' => [
|
||||
'type' => 'text',
|
||||
'pretty' => 'Stripe Publishable key',
|
||||
'default' => 'pk_xxxxxxxxxxxxxxx',
|
||||
],
|
||||
'stripeSecret' => [
|
||||
'type' => 'text',
|
||||
'pretty' => 'Stripe Secret key',
|
||||
'default' => 'sk_xxxxxxxxxxxxxxx',
|
||||
],
|
||||
];
|
||||
public static $webhookEvents = [
|
||||
'customer.subscription.created',
|
||||
'customer.subscription.updated',
|
||||
'customer.subscription.deleted',
|
||||
'customer.updated',
|
||||
'customer.deleted',
|
||||
'payment_method.automatically_updated',
|
||||
'invoice.payment_action_required',
|
||||
'invoice.payment_succeeded',
|
||||
'checkout.session.completed',
|
||||
];
|
||||
public static $userLinks = [
|
||||
"url" => "{ROOT_URL}member/manage",
|
||||
"name" => "Subscriptions"
|
||||
];
|
||||
|
||||
public function __construct( $load = false ) {
|
||||
if ( ! self::$loaded && $load ) {
|
||||
if ( App::$isLoggedIn ) {
|
||||
App::$isMember = $this->groupHasMemberAccess( App::$activeGroup );
|
||||
if ( empty( App::$isMember ) ) {
|
||||
App::$isMember = $this->userHasActiveMembership( App::$activeUser->ID );
|
||||
}
|
||||
}
|
||||
$this->filters[] = [
|
||||
'name' => 'member',
|
||||
'find' => '#{MEMBER}(.*?){/MEMBER}#is',
|
||||
'replace' => ( App::$isMember ? '$1' : '' ),
|
||||
'enabled' => true,
|
||||
];
|
||||
$this->filters[] = [
|
||||
'name' => 'nonmember',
|
||||
'find' => '#{NONMEMBER}(.*?){/NONMEMBER}#is',
|
||||
'replace' => ( App::$isLoggedIn && App::$isMember == false ? '$1' : '' ),
|
||||
'enabled' => true,
|
||||
];
|
||||
$this->filters[] = [
|
||||
'name' => 'upgrade',
|
||||
'find' => '#{UPGRADE}(.*?){/UPGRADE}#is',
|
||||
'replace' => ( App::$isLoggedIn && ( App::$isMember === 'monthly' ) ? '$1' : '' ),
|
||||
'enabled' => true,
|
||||
];
|
||||
}
|
||||
|
||||
parent::__construct( $load );
|
||||
|
||||
if ( $this->checkEnabled() && App::$isLoggedIn ) {
|
||||
if ( ! self::$loaded && $load ) {
|
||||
App::$userCPlinks[] = (object) self::$userLinks;
|
||||
App::$topNavRightDropdown .= '<li><a href="{ROOT_URL}member/manage" class="dropdown-item"><i class="fa fa-fw fa-credit-card"></i> Subscriptions</a></li>';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! self::$loaded && $load ) {
|
||||
self::$loaded = true;
|
||||
}
|
||||
|
||||
$api_key = Config::getValue( 'memberships/stripeSecret' );
|
||||
if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) {
|
||||
self::$stripe = false;
|
||||
} else {
|
||||
self::$stripe = new StripeClient( $api_key );
|
||||
}
|
||||
}
|
||||
|
||||
public function groupHasMemberAccess( $activeGroup ) {
|
||||
if ( $activeGroup->memberAccess == true ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function userHasActiveMembership( $user_id ) {
|
||||
$memberships = new Memberships;
|
||||
$membership = $memberships->findActiveByUserID( $user_id );
|
||||
if ( empty( $membership ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$products = new Products;
|
||||
$product = $products->findByPriceID( $membership->subscription_price_id );
|
||||
if ( empty( $product ) ) {
|
||||
Debug::error('Active membership on non-existent product');
|
||||
return false;
|
||||
}
|
||||
if ( $product->stripe_price_monthly == $membership->subscription_price_id ) {
|
||||
return 'monthly';
|
||||
}
|
||||
return 'yearly';
|
||||
}
|
||||
|
||||
public static function webhookSetup() {
|
||||
$root = Routes::getAddress();
|
||||
$response = self::$stripe->webhookEndpoints->create([
|
||||
'enabled_events' => self::$webhookEvents,
|
||||
'url' => $root . 'api/stripe/webhook',
|
||||
]);
|
||||
return $response;
|
||||
}
|
||||
|
||||
public static function webhookList() {
|
||||
$response = self::$stripe->webhookEndpoints->all(['limit' => 25]);
|
||||
return $response;
|
||||
}
|
||||
|
||||
public static function webhookRemove( $id ) {
|
||||
$response = self::$stripe->webhookEndpoints->delete( $id, []);
|
||||
return $response;
|
||||
}
|
||||
|
||||
public static function orphanAbandon( $id ) {
|
||||
$response = self::$stripe->prices->update(
|
||||
$id,
|
||||
['lookup_key' => ""]
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
|
||||
public static function findOrphans() {
|
||||
$orphans = [];
|
||||
// any prices with keys not represented in our local db will show as an orphan
|
||||
|
||||
$result = self::$stripe->prices->search([
|
||||
'query' => 'lookup_key:"membership-monthly"',
|
||||
]);
|
||||
|
||||
$products = new Products;
|
||||
if ( ! empty( $result->data ) ) {
|
||||
$product = $products->findByPriceID( $result->data[0]->id );
|
||||
|
||||
if ( empty( $product ) ) {
|
||||
$data = $result->data[0];
|
||||
|
||||
$child = new \stdClass;
|
||||
$child->price_id = $data->id;
|
||||
$child->product = $data->product;
|
||||
$child->lookup_key = $data->lookup_key;
|
||||
$child->amount = $data->unit_amount;
|
||||
$orphans[] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
$result = self::$stripe->prices->search([
|
||||
'query' => 'lookup_key:"membership-yearly"',
|
||||
]);
|
||||
if ( ! empty( $result->data ) ) {
|
||||
$product = $products->findByPriceID( $result->data[0]->id );
|
||||
|
||||
if ( empty( $product ) ) {
|
||||
$data = $result->data[0];
|
||||
|
||||
$child = new \stdClass;
|
||||
$child->price_id = $data->id;
|
||||
$child->product = $data->product;
|
||||
$child->lookup_key = $data->lookup_key;
|
||||
$child->amount = $data->unit_amount;
|
||||
$orphans[] = $child;
|
||||
}
|
||||
}
|
||||
|
||||
return $orphans;
|
||||
}
|
||||
}
|
47
app/plugins/members/views/admin/memberships/list.html
Normal file
47
app/plugins/members/views/admin/memberships/list.html
Normal file
@ -0,0 +1,47 @@
|
||||
<div class="context-main-bg context-main p-3">
|
||||
<legend class="text-center">Memberships</legend>
|
||||
<hr>
|
||||
{ADMIN_BREADCRUMBS}
|
||||
<form action="{ROOT_URL}admin/products/delete" method="post">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%">Name</th>
|
||||
<th style="width: 20%">status</th>
|
||||
<th style="width: 20%">Price</th>
|
||||
<th style="width: 10%">Start</th>
|
||||
<th style="width: 10%">End</th>
|
||||
<th style="width: 5%"></th>
|
||||
<th style="width: 5%"></th>
|
||||
<th style="width: 5%">
|
||||
<input type="checkbox" onchange="checkAll(this)" name="check.f" value="F_[]">
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{LOOP}
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td>{status}</td>
|
||||
<td>{prettyPrice}</td>
|
||||
<td>{DTC}{current_period_start}{/DTC}</td>
|
||||
<td>{DTC}{current_period_end}{/DTC}</td>
|
||||
<td><a href="{ROOT_URL}admin/records/view/{ID}" class="btn btn-sm btn-primary"><i class="fa fa-fw fa-upload"></i></a></td>
|
||||
<td><a href="{ROOT_URL}admin/records/delete/{ID}" class="btn btn-sm btn-danger"><i class="fa fa-fw fa-trash"></i></a></td>
|
||||
<td>
|
||||
<input type="checkbox" value="{ID}" name="MP_[]">
|
||||
</td>
|
||||
</tr>
|
||||
{/LOOP}
|
||||
{ALT}
|
||||
<tr>
|
||||
<td align="center" colspan="6">
|
||||
No results to show.
|
||||
</td>
|
||||
</tr>
|
||||
{/ALT}
|
||||
</tbody>
|
||||
</table>
|
||||
<button name="submit" value="submit" type="submit" class="btn btn-sm btn-danger"><i class="fa fa-fw fa-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
76
app/plugins/members/views/admin/memberships/view.html
Normal file
76
app/plugins/members/views/admin/memberships/view.html
Normal file
@ -0,0 +1,76 @@
|
||||
<div class="container py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
{ADMIN_BREADCRUMBS}
|
||||
<div class="card shadow">
|
||||
<!-- Card Header -->
|
||||
<div class="card-header text-center bg-dark text-white">
|
||||
<h3 class="card-title mb-0">{name}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Card Body -->
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<!-- Details -->
|
||||
<table class="table table-borderless">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" class="col-3">User</th>
|
||||
<td>{name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Status</th>
|
||||
<td>{status}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Billing Frequency</th>
|
||||
<td>{billing_frequency}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Started:</th>
|
||||
<td>{DTC}{current_period_end}{/DTC}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Ended:</th>
|
||||
<td>{DTC}{current_period_start}{/DTC}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Stripe Customer</th>
|
||||
<td class="input-group mb-3">
|
||||
<input type="text" value="{stripe_customer}" name="input" class="form-control" id="stripeCustomer{ID}">
|
||||
<button class="btn btn-secondary" onclick="copyElementText('stripeCustomer{ID}')">Copy</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Stripe Subscription</th>
|
||||
<td class="input-group mb-3">
|
||||
<input type="text" value="{stripe_subscription}" name="input" class="form-control" id="stripeSubscription{ID}">
|
||||
<button class="btn btn-secondary" onclick="copyElementText('stripeSubscription{ID}')">Copy</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Stripe Subscription Price</th>
|
||||
<td class="input-group mb-3">
|
||||
<input type="text" value="{subscription_price_id}" name="input" class="form-control" id="stripeSubscriptionPrice{ID}">
|
||||
<button class="btn btn-secondary" onclick="copyElementText('stripeSubscriptionPrice{ID}')">Copy</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Controls -->
|
||||
<div class="card-footer text-center">
|
||||
{ADMIN}
|
||||
<form action="{ROOT_URL}admin/feedback/delete" method="post">
|
||||
<input type="hidden" name="F_" value="{ID}">
|
||||
<input type="hidden" name="token" value="{TOKEN}">
|
||||
<button name="submit" value="submit" type="submit" class="btn btn-sm btn-danger"><i class="fa fa-fw fa-trash"></i></button>
|
||||
</form>
|
||||
{/ADMIN}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
35
app/plugins/members/views/admin/orphans.html
Normal file
35
app/plugins/members/views/admin/orphans.html
Normal file
@ -0,0 +1,35 @@
|
||||
<div class="context-main-bg context-main p-3">
|
||||
<legend class="text-center">Stripe Webhook Generation</legend>
|
||||
<hr>
|
||||
{ADMIN_BREADCRUMBS}
|
||||
<p>Orphans are stripe prices that have unique lookup_keys used by the membership system, but have no products currently saved.</p>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 10%">price id</th>
|
||||
<th style="width: 10%">amount</th>
|
||||
<th style="width: 50%">lookup key</th>
|
||||
<th style="width: 10%">url</th>
|
||||
<th style="width: 10%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{LOOP}
|
||||
<tr>
|
||||
<td>{price_id}</td>
|
||||
<td>{amount}</td>
|
||||
<td>{lookup_key}</td>
|
||||
<td><a href="{ROOT_URL}admin/members/orphans/adopt/{price_id}" class="btn btn-sm btn-success"><i class="fa fa-fw fa-download"></i></a></td>
|
||||
<td><a href="{ROOT_URL}admin/members/orphans/abandon/{price_id}" class="btn btn-sm btn-danger"><i class="fa fa-fw fa-trash"></i></a></td>
|
||||
</tr>
|
||||
{/LOOP}
|
||||
{ALT}
|
||||
<tr>
|
||||
<td class="text-center" colspan="5">
|
||||
No results to show.
|
||||
</td>
|
||||
</tr>
|
||||
{/ALT}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
51
app/plugins/members/views/admin/products/create.html
Normal file
51
app/plugins/members/views/admin/products/create.html
Normal file
@ -0,0 +1,51 @@
|
||||
<div class="context-main-bg context-main p-3">
|
||||
<legend class="text-center">Create Membership Product</legend>
|
||||
<hr>
|
||||
{ADMIN_BREADCRUMBS}
|
||||
<form action="" method="post">
|
||||
<fieldset>
|
||||
<!-- Name -->
|
||||
<div class="mb-3 row">
|
||||
<label for="name" class="col-lg-3 col-form-label text-end">Name:</label>
|
||||
<div class="col-lg-6">
|
||||
<input type="text" class="form-control" name="name" id="name">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Monthly Price -->
|
||||
<div class="mb-3 row">
|
||||
<label for="monthly_price" class="col-lg-3 col-form-label text-end">Monthly Price:</label>
|
||||
<div class="col-lg-6">
|
||||
<input type="number" class="form-control" name="monthly_price" id="monthly_price">
|
||||
<small class="form-text text-muted">Integer required: $4.99 : 499</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Yearly Price -->
|
||||
<div class="mb-3 row">
|
||||
<label for="yearly_price" class="col-lg-3 col-form-label text-end">Yearly Price:</label>
|
||||
<div class="col-lg-6">
|
||||
<input type="number" class="form-control" name="yearly_price" id="yearly_price">
|
||||
<small class="form-text text-muted">Integer required: $4.99 : 499</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-3 row">
|
||||
<label for="description" class="col-lg-3 col-form-label text-end">Description:</label>
|
||||
<div class="col-lg-6">
|
||||
<textarea class="form-control" name="description" id="description" rows="6" maxlength="2000" required></textarea>
|
||||
<small class="form-text text-muted">Max: 2000 characters</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Token -->
|
||||
<input type="hidden" name="token" value="{TOKEN}">
|
||||
</fieldset>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="text-center">
|
||||
<button type="submit" name="submit" value="submit" class="btn btn-primary btn-lg center-block">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
51
app/plugins/members/views/admin/products/edit.html
Normal file
51
app/plugins/members/views/admin/products/edit.html
Normal file
@ -0,0 +1,51 @@
|
||||
<div class="context-main-bg context-main p-3">
|
||||
<legend class="text-center">Create Membership Product</legend>
|
||||
<hr>
|
||||
{ADMIN_BREADCRUMBS}
|
||||
<form action="" method="post">
|
||||
<fieldset>
|
||||
<!-- Name -->
|
||||
<div class="mb-3 row">
|
||||
<label for="name" class="col-lg-3 col-form-label text-end">Name:</label>
|
||||
<div class="col-lg-6">
|
||||
<input type="text" class="form-control" name="name" id="name" value="{name}">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Monthly Price -->
|
||||
<div class="mb-3 row">
|
||||
<label for="monthly_price" class="col-lg-3 col-form-label text-end">Monthly Price:</label>
|
||||
<div class="col-lg-6">
|
||||
<input type="number" class="form-control" name="monthly_price" id="monthly_price" value="{monthly_price}">
|
||||
<small class="form-text text-muted">Integer required: $4.99 : 499</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Yearly Price -->
|
||||
<div class="mb-3 row">
|
||||
<label for="yearly_price" class="col-lg-3 col-form-label text-end">Yearly Price:</label>
|
||||
<div class="col-lg-6">
|
||||
<input type="number" class="form-control" name="yearly_price" id="yearly_price" value="{yearly_price}">
|
||||
<small class="form-text text-muted">Integer required: $4.99 : 499</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-3 row">
|
||||
<label for="description" class="col-lg-3 col-form-label text-end">Description:</label>
|
||||
<div class="col-lg-6">
|
||||
<textarea class="form-control" name="description" id="description" rows="6" maxlength="2000" required>{description}</textarea>
|
||||
<small class="form-text text-muted">Max: 2000 characters</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Token -->
|
||||
<input type="hidden" name="token" value="{TOKEN}">
|
||||
</fieldset>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="text-center">
|
||||
<button type="submit" name="submit" value="submit" class="btn btn-primary btn-lg center-block">Send</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
46
app/plugins/members/views/admin/products/list.html
Normal file
46
app/plugins/members/views/admin/products/list.html
Normal file
@ -0,0 +1,46 @@
|
||||
<div class="context-main-bg context-main p-3">
|
||||
<legend class="text-center">Membership Products</legend>
|
||||
<hr>
|
||||
{ADMIN_BREADCRUMBS}
|
||||
<form action="{ROOT_URL}admin/products/delete" method="post">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 10%">Name</th>
|
||||
<th style="width: 25%">Monthly Price</th>
|
||||
<th style="width: 50%">Yearly Price</th>
|
||||
<th style="width: 5%"></th>
|
||||
<th style="width: 5%"></th>
|
||||
<th style="width: 5%"></th>
|
||||
<th style="width: 5%">
|
||||
<input type="checkbox" onchange="checkAll(this)" name="check.f" value="F_[]">
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{LOOP}
|
||||
<tr>
|
||||
<td>{name}</td>
|
||||
<td>{monthly_price}</td>
|
||||
<td>{yearly_price}</td>
|
||||
<td><a href="{ROOT_URL}admin/products/view/{ID}" class="btn btn-sm btn-primary"><i class="fa fa-fw fa-upload"></i></a></td>
|
||||
<td><a href="{ROOT_URL}admin/products/edit/{ID}" class="btn btn-sm btn-warning"><i class="fa fa-fw fa-pencil"></i></a></td>
|
||||
<td><a href="{ROOT_URL}admin/products/delete/{ID}" class="btn btn-sm btn-danger"><i class="fa fa-fw fa-trash"></i></a></td>
|
||||
<td>
|
||||
<input type="checkbox" value="{ID}" name="MP_[]">
|
||||
</td>
|
||||
</tr>
|
||||
{/LOOP}
|
||||
{ALT}
|
||||
<tr>
|
||||
<td align="center" colspan="6">
|
||||
No results to show.
|
||||
</td>
|
||||
</tr>
|
||||
{/ALT}
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="{ROOT_URL}admin/products/create" class="btn btn-sm btn-primary">Create</a>
|
||||
<button name="submit" value="submit" type="submit" class="btn btn-sm btn-danger"><i class="fa fa-fw fa-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
72
app/plugins/members/views/admin/products/view.html
Normal file
72
app/plugins/members/views/admin/products/view.html
Normal file
@ -0,0 +1,72 @@
|
||||
<div class="container py-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
{ADMIN_BREADCRUMBS}
|
||||
<div class="card shadow">
|
||||
<!-- Card Header -->
|
||||
<div class="card-header text-center bg-dark text-white">
|
||||
<h3 class="card-title mb-0">{name}</h3>
|
||||
</div>
|
||||
|
||||
<!-- Card Body -->
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<!-- Log Details -->
|
||||
<table class="table table-borderless">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" class="col-3">Name</th>
|
||||
<td>{name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Monthly Price</th>
|
||||
<td>{monthly_price}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Yearly Price</th>
|
||||
<td>{yearly_price}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Stripe Product</th>
|
||||
<td class="input-group mb-3">
|
||||
<input type="text" value="{stripe_product}" name="input" class="form-control" id="stripeProduct{ID}">
|
||||
<button class="btn btn-secondary" onclick="copyElementText('stripeProduct{ID}')" aria-describedby="stripeProduct{ID}">Copy</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Stripe Price Yearly</th>
|
||||
<td class="input-group mb-3">
|
||||
<input type="text" value="{stripe_price_yearly}" name="input" class="form-control" id="stripeYearly{ID}">
|
||||
<button class="btn btn-secondary" onclick="copyElementText('stripeYearly{ID}')">Copy</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Stripe Price Monthly</th>
|
||||
<td class="input-group mb-3">
|
||||
<input type="text" value="{stripe_price_monthly}" name="input" class="form-control" id="stripeMonthly{ID}">
|
||||
<button class="btn btn-secondary" onclick="copyElementText('stripeMonthly{ID}')">Copy</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Description</th>
|
||||
<td>{description}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Controls -->
|
||||
<div class="card-footer text-center">
|
||||
{ADMIN}
|
||||
<form action="{ROOT_URL}admin/feedback/delete" method="post">
|
||||
<input type="hidden" name="F_" value="{ID}">
|
||||
<input type="hidden" name="token" value="{TOKEN}">
|
||||
<button name="submit" value="submit" type="submit" class="btn btn-sm btn-danger"><i class="fa fa-fw fa-trash"></i></button>
|
||||
</form>
|
||||
{/ADMIN}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
13
app/plugins/members/views/admin/scripts.html
Normal file
13
app/plugins/members/views/admin/scripts.html
Normal file
@ -0,0 +1,13 @@
|
||||
<div class="context-main-bg context-main p-3">
|
||||
<legend class="text-center">Membership Scripts</legend>
|
||||
<hr>
|
||||
{ADMIN_BREADCRUMBS}
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<a href="{ROOT_URL}admin/members/webhooks">Webhook Generation</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{ROOT_URL}admin/members/orphans">Orphan Products</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
43
app/plugins/members/views/admin/webhooks.html
Normal file
43
app/plugins/members/views/admin/webhooks.html
Normal file
@ -0,0 +1,43 @@
|
||||
<div class="context-main-bg context-main p-3">
|
||||
<legend class="text-center">Stripe Webhook Generation</legend>
|
||||
<hr>
|
||||
{ADMIN_BREADCRUMBS}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 10%">id</th>
|
||||
<th style="width: 10%">status</th>
|
||||
<th style="width: 50%">enabled_events</th>
|
||||
<th style="width: 10%">url</th>
|
||||
<th style="width: 10%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{LOOP}
|
||||
<tr>
|
||||
<td>{id}</td>
|
||||
<td>{status}</td>
|
||||
<td>{enabled_events}</td>
|
||||
<td>{url}</td>
|
||||
<td><a href="{ROOT_URL}admin/members/webhooks/delete/{ID}" class="btn btn-sm btn-danger"><i class="fa fa-fw fa-trash"></i></a></td>
|
||||
</tr>
|
||||
{/LOOP}
|
||||
{ALT}
|
||||
<tr>
|
||||
<td class="text-center" colspan="5">
|
||||
No results to show.
|
||||
</td>
|
||||
</tr>
|
||||
{/ALT}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="text-center">
|
||||
<h5>WARNING: Regenerating existing webhooks makes joey sad, don't do iit!</h5>
|
||||
<p>The new webhooks will be generated using the base url: <strong>{urltouse}</strong></p>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<button name="submit" value="submit" type="submit" class="btn btn-lg btn-danger">re-generate</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
20
app/plugins/members/views/cancel.html
Normal file
20
app/plugins/members/views/cancel.html
Normal file
@ -0,0 +1,20 @@
|
||||
<div class="container py-4 context-main-bg my-4">
|
||||
<h2 class="text-center">Are You Sure You Want to Cancel?</h2>
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<p class="lead">
|
||||
Cancelling your subscription means you'll miss out on exclusive features, updates, and benefits.
|
||||
</p>
|
||||
<p>
|
||||
Consider staying to continue enjoying the full experience of our service.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-3 offset-3">
|
||||
<a href="{ROOT_URL}member/cancelconfirm/{cancelid}" class="btn btn-lg btn-outline-danger btn-block">Cancel Subscription</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="{ROOT_URL}member/manage" class="btn btn-lg btn-primary btn-block atb-green-bg">Go Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
130
app/plugins/members/views/join.html
Normal file
130
app/plugins/members/views/join.html
Normal file
@ -0,0 +1,130 @@
|
||||
<!-- Compare plans -->
|
||||
<div class="table-responsive pricing-container container pb-4" id="compare">
|
||||
<h1 class="display-6 text-center my-4">Compare plans</h1>
|
||||
<table class="table text-center context-main border-white">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 34%;"></th>
|
||||
<th style="width: 22%;">Free</th>
|
||||
<th style="width: 22%;">Monthly</th>
|
||||
<th style="width: 22%;">Yearly</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row" class="text-start">Add and Manage Bookmarks</th>
|
||||
<td><i class="fa fa-fw fa-check"></i></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-start">Extensions for all major browsers</th>
|
||||
<td><i class="fa fa-fw fa-check"></i></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-start">Access from any device</th>
|
||||
<td><i class="fa fa-fw fa-check"></i></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-start">Share bookmarks and folders</th>
|
||||
<td><i class="fa fa-fw fa-check"></i></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-start">Import/Export Features</th>
|
||||
<td></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-start">Customizable Dashboards / Pages</th>
|
||||
<td></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-start">Request/Influence Development</th>
|
||||
<td></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-start">Early Access</th>
|
||||
<td></td>
|
||||
<td><i class="fa fa-fw fa-check atb-green"></i></td>
|
||||
<td><i class="fa-solid fa-check atb-green"></i></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row" class="text-start">Cheaper</th>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><i class="fa-solid fa-check atb-green"></i></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="b-example-divider"></div>
|
||||
|
||||
<!-- Plan Choices -->
|
||||
<div class="d-flex justify-content-center" id="pricing">
|
||||
<div class="pricing-container container row row-cols-1 row-cols-md-3 my-5 text-center">
|
||||
<div class="col">
|
||||
<div class="card mb-4 rounded-3 shadow-sm h-100 context-main-bg">
|
||||
<div class="card-header py-3">
|
||||
<h4 class="my-0 fw-normal">Free</h4>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h1 class="card-title pricing-card-title">$0<small class="text-muted fw-light">/mo</small></h1>
|
||||
<ul class="list-unstyled mt-3 mb-4">
|
||||
<li>Add / Manage your bookmarks</li>
|
||||
<li>Extensions for all major browsers</li>
|
||||
<li>Access from any device</li>
|
||||
<li>Share access with anyone</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card mb-4 rounded-3 shadow-sm h-100 context-main-bg">
|
||||
<div class="card-header py-3">
|
||||
<h4 class="my-0 fw-normal">Monthly</h4>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h1 class="card-title pricing-card-title">{prettyPriceMonthly}<small class="text-muted fw-light">/month</small></h1>
|
||||
<ul class="list-unstyled mt-3 mb-4">
|
||||
<li>Import/Export Features</li>
|
||||
<li>Integration with TempusTools App (WIP)</li>
|
||||
<li>Customizable Dashboards / Pages</li>
|
||||
<li>Direct control of Feature Development</li>
|
||||
<li>Early Access to new features</li>
|
||||
</ul>
|
||||
<a href="/member/checkout/monthly" class="mt-auto w-100 btn btn-lg atb-green-bg">
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card mb-4 rounded-3 shadow-sm atb-green-outline-only h-100 context-main-bg">
|
||||
<div class="card-header py-3 atb-green-bg">
|
||||
<h4 class="my-0 fw-normal">Yearly</h4>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h1 class="card-title pricing-card-title">{prettyPriceYearly}<small class="text-muted fw-light">/year</small></h1>
|
||||
<ul class="list-unstyled mt-3 mb-4">
|
||||
<li>Its cheaper if you like the product</li>
|
||||
</ul>
|
||||
<a href="/member/checkout/yearly" class="mt-auto w-100 btn btn-lg atb-green-bg">
|
||||
Get started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
52
app/plugins/members/views/manage.html
Normal file
52
app/plugins/members/views/manage.html
Normal file
@ -0,0 +1,52 @@
|
||||
<div class="container py-4 context-main-bg my-4">
|
||||
<h3 class="mb-4 text-center">Manage Memberships</h3>
|
||||
<hr>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<table class="table text-center context-main">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%">Name</th>
|
||||
<th style="width: 20%">Status</th>
|
||||
<th style="width: 20%">Price</th>
|
||||
<th style="width: 10%">Start</th>
|
||||
<th style="width: 10%">End</th>
|
||||
<th style="width: 5%"></th>
|
||||
<th style="width: 5%"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{LOOP}
|
||||
<tr>
|
||||
<td>{productName}</td>
|
||||
<td>{status}</td>
|
||||
<td>{prettyPrice}</td>
|
||||
<td>{DTC=date}{current_period_start}{/DTC}</td>
|
||||
<td>{DTC=date}{current_period_end}{/DTC}</td>
|
||||
<td>
|
||||
<a href="{ROOT_URL}member/pause/{ID}" class="btn btn-sm btn-outline-warning">
|
||||
<i class="fa fa-fw fa-pause"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{ROOT_URL}member/cancel/{ID}" class="btn btn-sm btn-outline-danger">
|
||||
<i class="fa fa-fw fa-ban"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/LOOP}
|
||||
{ALT}
|
||||
<tr>
|
||||
<td colspan="7" class="text-center">
|
||||
No results to show.
|
||||
</td>
|
||||
</tr>
|
||||
{/ALT}
|
||||
</tbody>
|
||||
</table>
|
||||
<a href="{ROOT_URL}member/managepayment" class="btn btn-sm btn-primary atb-green-bg">
|
||||
Manage Payment Method
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
18
app/plugins/members/views/members.html
Normal file
18
app/plugins/members/views/members.html
Normal file
@ -0,0 +1,18 @@
|
||||
<div class="col-8 mx-auto p-4 rounded shadow-sm context-main-bg my-4">
|
||||
<h2 class="text-center atb-green mb-4">Membership Benefits</h2>
|
||||
<p class="lead">
|
||||
First, let me say thank you for choosing to become a member! There are several great benefits exclusively for members.
|
||||
</p>
|
||||
<p>
|
||||
In addition to extra features for bookmark management like <a class="text-decoration-none atb-green" href="{ROOT_URL}bookmarks/dashboards">dashboards</a> and <a class="text-decoration-none atb-green" href="{ROOT_URL}bookmarks/import">import</a>/<a class="text-decoration-none atb-green" href="{ROOT_URL}bookmarks/export">export</a>, you gain access to influence development. <a class="text-decoration-none atb-green" href="{ROOT_URL}suggestions">Suggestions</a> gives users a direct way to make suggestions to me personally. By default, suggestions are not public, but I can comment on them to let you know what I think.
|
||||
All respectful and reasonable suggestions go up for the entire community to see and comment on. There iis going to be a loyalty points system in the future to allow you to accrue points and use the points to vote for suggestions and features they like. But that's for another time.
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
Right now, this entire system was built and managed by myself. I have used my own version of this for years, but translating it to a publicly available product is not a 1-to-1 job. There may be bugs or issues encountered while you use the product. I can't guarantee a fix for every need in every case immediately, but I do actively keep track of bugs and work hard to ensure everyone has a great experience using the app.
|
||||
</p>
|
||||
<div class="text-center mt-4 pb-4">
|
||||
{loggedin}<a href="/bugreport" class="btn btn-outline-secondary btn-lg px-5 ms-3 atb-green-outline">Report a Bug</a>{/loggedin}
|
||||
<a href="/member/manage" class="btn btn-outline-secondary btn-lg px-5 ms-3 atb-green-bg">Manage Membership</a>
|
||||
<a href="/contact" class="btn btn-outline-secondary btn-lg px-5 ms-3 atb-green-outline">Contact Us</a>
|
||||
</div>
|
||||
</div>
|
20
app/plugins/members/views/pause.html
Normal file
20
app/plugins/members/views/pause.html
Normal file
@ -0,0 +1,20 @@
|
||||
<div class="container py-4 context-main-bg my-4">
|
||||
<h2 class="text-center">Are You Sure You Want to Pause?</h2>
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<p class="lead">
|
||||
Pausing your subscription means you'll miss out on exclusive features, updates, and benefits.
|
||||
</p>
|
||||
<p>
|
||||
Consider staying to continue enjoying the full experience of our service.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-3 offset-3">
|
||||
<a href="{ROOT_URL}member/pauseconfirm/{pauseid}" class="btn btn-lg btn-outline-danger btn-block">Pause Subscription</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<a href="{ROOT_URL}member/manage" class="btn btn-lg btn-primary btn-block atb-green-bg">Go Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
14
app/plugins/members/views/paymentcanceled.html
Normal file
14
app/plugins/members/views/paymentcanceled.html
Normal file
@ -0,0 +1,14 @@
|
||||
<div class="col-8 mx-auto p-4 rounded shadow-sm context-main-bg my-4">
|
||||
<h2 class="text-center atb-green mb-4">Almost there!</h2>
|
||||
<p class="lead">
|
||||
Take your time, its not a sprint, its a marathon.
|
||||
Nno-one wants to checkout too fast, its embarrassing.
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
If you think this is a tool that you could really use and can't afford it, use the contact form below and tell me why you need it.
|
||||
</p>
|
||||
<div class="text-center mt-4 pb-4">
|
||||
{loggedin}<a href="/bugreport" class="btn btn-primary btn-lg px-5 atb-green-bg">Report a Bug</a>{/loggedin}
|
||||
<a href="/contact" class="btn btn-outline-secondary btn-lg px-5 ms-3 atb-green-outline">Contact Us</a>
|
||||
</div>
|
||||
</div>
|
14
app/plugins/members/views/paymentcomplete.html
Normal file
14
app/plugins/members/views/paymentcomplete.html
Normal file
@ -0,0 +1,14 @@
|
||||
<div class="col-8 mx-auto p-4 rounded shadow-sm context-main-bg my-4">
|
||||
<h2 class="text-center atb-green mb-4">Thanks for joining!</h2>
|
||||
<p class="lead">
|
||||
Its people like you who keep the pixels on around here.
|
||||
Its people like me who tell people like you that unfortunately, it could take up to an hour for your membership to activate.
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
With that said, its usually instant, and if its taking too long, just reach out to us via the contact form.
|
||||
</p>
|
||||
<div class="text-center mt-4 pb-4">
|
||||
{loggedin}<a href="/bugreport" class="btn btn-primary btn-lg px-5 atb-green-bg">Report a Bug</a>{/loggedin}
|
||||
<a href="/contact" class="btn btn-outline-secondary btn-lg px-5 ms-3 atb-green-outline">Contact Us</a>
|
||||
</div>
|
||||
</div>
|
69
app/plugins/members/views/register.html
Normal file
69
app/plugins/members/views/register.html
Normal file
@ -0,0 +1,69 @@
|
||||
<form action="" method="post" class="container py-4">
|
||||
<h2 class="text-center mb-4">Create an Account</h2>
|
||||
<p class="text-center">After registration is complete, you will be redirected to Stripe to handle payment.</p>
|
||||
<p class="text-center">
|
||||
You have selected the <strong>{planName}</strong> plan at <em>{prettyPrice}</em>.
|
||||
</p>
|
||||
<fieldset>
|
||||
<!-- Username -->
|
||||
<div class="mb-3 row">
|
||||
<label for="username" class="col-lg-6 col-form-label text-end">Username:</label>
|
||||
<div class="col-lg-2">
|
||||
<input type="text" class="form-control" name="username" id="username" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Email -->
|
||||
<div class="mb-3 row">
|
||||
<label for="email" class="col-lg-6 col-form-label text-end">Email:</label>
|
||||
<div class="col-lg-2">
|
||||
<input type="email" class="form-control" name="email" id="email" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Re-enter Email -->
|
||||
<div class="mb-3 row">
|
||||
<label for="email2" class="col-lg-6 col-form-label text-end">Re-Enter Email:</label>
|
||||
<div class="col-lg-2">
|
||||
<input type="email" class="form-control" name="email2" id="email2" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="mb-3 row">
|
||||
<label for="password" class="col-lg-6 col-form-label text-end">Password:</label>
|
||||
<div class="col-lg-2">
|
||||
<input type="password" class="form-control" name="password" id="password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Re-enter Password -->
|
||||
<div class="mb-3 row">
|
||||
<label for="password2" class="col-lg-6 col-form-label text-end">Re-Enter Password:</label>
|
||||
<div class="col-lg-2">
|
||||
<input type="password" class="form-control" name="password2" id="password2" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Terms of Service -->
|
||||
<div class="mb-3 text-center">
|
||||
<div class="">
|
||||
<input type="checkbox" class="form-check-input" name="terms" id="terms" value="1" required>
|
||||
<label for="terms" class="form-check-label">
|
||||
I have read and agree to the <a href="/home/terms" class="text-primary">Terms of Service</a>
|
||||
</label>
|
||||
</div>
|
||||
<div class="terms mt-2 mx-auto">
|
||||
{TERMS}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden Token -->
|
||||
<input type="hidden" name="token" value="{TOKEN}">
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="text-center">
|
||||
<button type="submit" name="submit" value="submit" class="btn btn-primary btn-lg">Sign up</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
24
app/plugins/members/views/upgrade.html
Normal file
24
app/plugins/members/views/upgrade.html
Normal file
@ -0,0 +1,24 @@
|
||||
<div class="col-8 mx-auto p-4 rounded shadow-sm mb-5 context-main-bg mt-4 text-center">
|
||||
<div class="row">
|
||||
<h2 class="mb-4 atb-green">Upgrade to a Yearly Plan</h2>
|
||||
<p class="lead">
|
||||
Save more and enjoy uninterrupted access to all features with our yearly plan!
|
||||
</p>
|
||||
<p>
|
||||
Upgrading now means you'll save <strong> nearly $40 a year</strong> compared to the monthly plan.
|
||||
Stay committed and make the most of our service.
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<a href="/member/checkout/upgrade" class="btn btn-lg btn-block atb-green-bg">
|
||||
Upgrade to Yearly
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<a href="/member" class="btn btn-lg btn-block atb-green-outline">
|
||||
Stay on Monthly
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
13
app/plugins/members/views/upgradeCompleted.html
Normal file
13
app/plugins/members/views/upgradeCompleted.html
Normal file
@ -0,0 +1,13 @@
|
||||
<div class="col-8 mx-auto p-4 rounded shadow-sm context-main-bg my-4">
|
||||
<h2 class="text-center atb-green mb-4">Thanks for the vote of confidence!</h2>
|
||||
<p class="lead">
|
||||
While you were more profitable as a monthly user, I do appreciate the vote of confidence it takes to commit to 4x the cost at once.
|
||||
</p>
|
||||
<p class="text-muted">
|
||||
There really is no more benefit than you saving money, so enjoy the features you have already come to know and enjoy.
|
||||
</p>
|
||||
<div class="text-center mt-4 pb-4">
|
||||
{loggedin}<a href="/bugreport" class="btn btn-primary btn-lg px-5 atb-green-bg">Report a Bug</a>{/loggedin}
|
||||
<a href="/contact" class="btn btn-outline-secondary btn-lg px-5 ms-3 atb-green-outline">Contact Us</a>
|
||||
</div>
|
||||
</div>
|
Reference in New Issue
Block a user