Add donate and memberships

This commit is contained in:
Joey Kimsey
2024-12-05 15:33:25 -05:00
parent 03aedc3020
commit 402182714e
39 changed files with 2109 additions and 21 deletions

View 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 ) {
}
}

View File

@ -0,0 +1,40 @@
<?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;
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 Webhooks';
if ( !Input::exists( 'submit' ) ) {
return Views::view( 'members.admin.webhooks' );
}
MemberModel::webhookSetup();
Issues::add( 'success', 'Webhooks Generated' );
Issues::add( 'error', 'Now, LEAVE!' );
}
}

View 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::$posts->findById( $id ) );
}
if ( !Forms::check( 'editMembershipProduct' ) ) {
Issues::add( 'error', [ 'There was an error with your form.' => Check::userErrors() ] );
return $this->index();
}
if ( self::$posts->updatePost( $id, Input::post( 'title' ), Input::post( 'blogPost' ), Input::post( 'submit' ) ) === true ) {
Issues::add( 'success', 'Post 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( 'blog.admin.view', $data );
}
Issues::add( 'error', 'Post 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();
}
}

View File

@ -0,0 +1,50 @@
<?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( $data = null ) {
}
public function delete( $data = null ) {
}
public function preview( $data = null ) {
}
}

View 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\Controllers\StripeApiController;
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 StripeApiController {
public static $stripe;
public static $customers;
public static $memberships;
public function __construct() {
parent::__construct();
$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::v( $e );
http_response_code(400);
$responseType = 'error';
$response = 'Exception';
}
Views::view( 'api.response', ['response' => json_encode( [ $responseType => $response ], true )]);
}
}

View File

@ -18,19 +18,223 @@ 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;
class Member extends Controller {
public static $customers;
public static $products;
public function __construct() {
parent::__construct();
Template::noIndex();
if ( !App::$isMember ) {
Session::flash( 'error', 'You do not have permission to view this page.' );
return Redirect::home();
}
$api_key = Config::getValue( 'memberships/stripeSecret' );
self::$customers = new MembershipCustomers;
self::$products = new MembershipProducts;
}
public function index() {
self::$title = 'Members Area';
if ( !App::$isMember ) {
Session::flash( 'error', 'You do not have permission to view this page.' );
return Redirect::home();
}
Views::view( 'members.members' );
}
}
public function managepayment( $id = null ) {
$api_key = Config::getValue( 'memberships/stripeSecret' );
$stripe = new \Stripe\StripeClient( $api_key );
$customer = self::$customers->findOrCreate( App::$activeUser->ID );
if ( empty( $customer ) ) {
Session::flash( 'error', 'no customer.' );
return Redirect::to( 'member/manage' );
}
$session = $stripe->billingPortal->sessions->create([
'customer' => $customer,
'return_url' => Routes::getAddress() . 'member/manage',
]);
header('Location: ' . $session->url);
exit;
}
public function cancelconfirm( $id = null ) {
$memberships = new Memberships;
$result = $memberships->cancel( $id );
// dv( $result );
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 ) {
$memberships = new Memberships;
$result = $memberships->cancel( $id );
// dv( $result );
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 ) {
self::$title = 'pause Membership';
Components::set( 'pauseid', $id );
Views::view( 'members.pause' );
}
public function resume( $id = null ) {
self::$title = 'resume Membership';
Views::view( 'members.resume' );
}
public function cancel( $id = null ) {
self::$title = 'Cancel Membership';
Components::set( 'cancelid', $id );
Views::view( 'members.cancel' );
}
public function manage( $id = null ) {
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( $id = null ) {
self::$title = 'Upgrade Membership';
Views::view( 'members.upgrade' );
}
public function join( $id = null ) {
self::$title = 'Join {SIITENAME}!';
$product = self::$products->findById( $id );
if ( empty( $product ) ) {
Session::flash( 'success', 'We aren\'t currently accepting new members, please check back soon!' );
return Redirect::home();
}
Views::view( 'members.landing1', $product );
}
public function signup( $id = null ) {
self::$title = 'Sign-up for {SIITENAME}!';
$product = self::$products->findById( $id );
if ( empty( $product ) ) {
Session::flash( 'success', 'We aren\'t currently accepting new members, please check back soon!' );
return Redirect::home();
}
Views::view( 'members.landing2', $product );
}
public function getyearly( $id = null ) {
if ( empty( $id ) ) {
Issues::add( 'error', 'no id' );
return $this->index();
}
$product = self::$products->findById( $id );
if ( empty( $product ) ) {
Issues::add( 'error', 'no product' );
return $this->index();
}
$customer = self::$customers->findOrCreate( App::$activeUser->ID );
if ( empty( $customer ) ) {
Issues::add( 'error', 'no customer' );
return $this->index();
}
self::$title = 'Purchase';
$price = $product->stripe_price_yearly;
$api_key = Config::getValue( 'memberships/stripeSecret' );
$stripe = new \Stripe\StripeClient( $api_key );
$session = $stripe->checkout->sessions->create([
'payment_method_types' => ['card'],
'customer' => $customer,
'line_items' => [[
'price' => $price,
'quantity' => 1,
]],
'mode' => 'subscription',
'success_url' => Routes::getAddress() . 'member/paymentcomplete?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => Routes::getAddress() . 'member/paymentcanceled',
]);
header('Location: ' . $session->url);
exit;
}
public function getmonthly( $id = null ) {
if ( empty( $id ) ) {
Issues::add( 'error', 'no id' );
return $this->index();
}
$product = self::$products->findById( $id );
if ( empty( $product ) ) {
Issues::add( 'error', 'no product' );
return $this->index();
}
$customer = self::$customers->findOrCreate( App::$activeUser->ID );
if ( empty( $customer ) ) {
Issues::add( 'error', 'no customer' );
return $this->index();
}
self::$title = 'Purchase';
$price = $product->stripe_price_monthly;
$api_key = Config::getValue( 'memberships/stripeSecret' );
$stripe = new \Stripe\StripeClient( $api_key );
$session = $stripe->checkout->sessions->create([
'payment_method_types' => ['card'],
'customer' => $customer,
'line_items' => [[
'price' => $price,
'quantity' => 1,
]],
'mode' => 'subscription',
'success_url' => Routes::getAddress() . 'member/paymentcomplete?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => Routes::getAddress() . 'member/paymentcanceled',
]);
header('Location: ' . $session->url);
exit;
}
public function paymentcanceled() {
self::$title = '(almost) Members Area';
Views::view( 'members.paymentcanceled' );
}
public function paymentcomplete() {
self::$title = '(almost) Members Area';
Views::view( 'members.paymentcomplete' );
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* app/classes/admin_controller.php
*
* This is the base admin controller. Every other admin controller should
* extend this class.
*
* @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\TheTempusProject as App;
use TheTempusProject\Hermes\Functions\Redirect;
use TheTempusProject\Bedrock\Functions\Session;
use TheTempusProject\Classes\Controller;
class StripeApiController extends Controller {
public function __construct() {
parent::__construct();
// if ( ! App::verifyApiRequest() ) {
// Session::flash( 'error', 'You do not have permission to view this page.' );
// return Redirect::home();
// }
Template::noFollow();
Template::noIndex();
Template::addHeader( 'Content-Type: application/json; charset=utf-8' );
Template::setTemplate( 'api' );
}
}

View 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;

View 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( "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;
}
}
}

View 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();
}
}

View File

@ -0,0 +1,206 @@
<?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;
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, $4399.22 === 439922
[ 'yearly_price', 'int', '10' ], // must be int value greater than 99 IE $1.00 === 100, $4399.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();
$api_key = Config::getValue( 'memberships/stripeSecret' );
if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) {
Debug::error( "No Stripe Key found" );
} else {
self::$stripe = new \Stripe\StripeClient( $api_key );
}
}
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->id,
['lookup_key' => ""]
);
return $this->createStripeYearlyPrice( $product, $yearly_price );
}
public function updateStripeMonthlyPrice( $product, $monthly_price ) {
$stripe->prices->update(
$monthly->id,
['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 = '&#36;' . number_format( $instance->monthly_price / 100, 2 ); // Outputs: $99.49
$instance->prettyPriceYearly = '&#36;' . number_format( $instance->yearly_price / 100, 2 ); // Outputs: $99.49
$instance->prettySavings = '&#36;' . 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 $data->first();
}
}

View File

@ -0,0 +1,170 @@
<?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;
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();
self::$products = new MembershipProducts;
$api_key = Config::getValue( 'memberships/stripeSecret' );
if ( $api_key == 'sk_xxxxxxxxxxxxxxx' || empty($api_key) ) {
Debug::error( "No Stripe Key found" );
} else {
self::$stripe = new \Stripe\StripeClient( $api_key );
}
}
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 = '&#36;' . 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->getPaginated( $this->tableName, $whereClause );
} else {
$postData = self::$db->getPaginated( $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;
}
}

View File

@ -1,10 +1,10 @@
<?php
/**
* app/plugins/chat/plugin.php
* app/plugins/members/plugin.php
*
* This houses all of the main plugin info and functionality.
*
* @package TP Chat
* @package TP Members
* @version 3.0
* @author Joey Kimsey <Joey@thetempusproject.com>
* @link https://TheTempusProject.com
@ -14,10 +14,17 @@ 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;
class Members extends Plugin {
public static $stripe;
public static $memberships;
private static $loaded = false;
public $pluginName = 'TP Membership';
public $configName = 'membership';
public $configName = 'memberships';
public $pluginAuthor = 'JoeyK';
public $pluginWebsite = 'https://TheTempusProject.com';
public $modelVersion = '1.0';
@ -35,8 +42,21 @@ class Members extends Plugin {
];
public $admin_links = [
[
'text' => '<i class="fa fa-fw fa-lock"></i> Memberships',
'url' => '{ROOT_URL}admin/member',
'text' => '<i class="fa fa-fw fa-arrows-v"></i> Memberships',
'url' => [
[
'text' => '<i class="fa fa-fw fa-database"></i> Products',
'url' => '{ROOT_URL}admin/products',
],
[
'text' => '<i class="fa fa-fw fa-database"></i> Subscriptions',
'url' => '{ROOT_URL}admin/records',
],
[
'text' => '<i class="fa fa-fw fa-database"></i> Invoices',
'url' => '{ROOT_URL}admin/invoices',
],
],
],
];
public $main_links = [
@ -45,6 +65,11 @@ class Members extends Plugin {
'url' => '{ROOT_URL}member/index',
'filter' => 'member',
],
[
'text' => 'Become a Member',
'url' => '{ROOT_URL}member/join/1',
'filter' => 'nonmember',
],
];
public $resourceMatrix = [
'groups' => [
@ -54,11 +79,45 @@ class Members extends Plugin {
]
],
];
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 ) {
App::$userCPlinks[] = (object) self::$userLinks;
self::$loaded = true;
}
if ( App::$isLoggedIn ) {
App::$isMember = $this->hasMemberAccess( App::$activeGroup );
App::$isMember = $this->groupHasMemberAccess( App::$activeGroup );
if ( empty( App::$isMember ) ) {
App::$isMember = $this->hasMemberAccess( App::$activeUser );
App::$isMember = $this->userHasActiveMembership( App::$activeUser->ID );
}
}
$this->filters[] = [
@ -67,9 +126,54 @@ class Members extends Plugin {
'replace' => ( App::$isMember ? '$1' : '' ),
'enabled' => true,
];
$this->filters[] = [
'name' => 'nonmember',
'find' => '#{NONMEMBER}(.*?){/NONMEMBER}#is',
'replace' => ( App::$isLoggedIn && ! App::$isMember ? '$1' : '' ),
'enabled' => 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 );
}
parent::__construct( $load );
}
public function hasMemberAccess( $input ) {
public function groupHasMemberAccess( $activeGroup ) {
if ( $activeGroup->memberAccess == true ) {
return true;
}
return false;
}
public function userHasActiveMembership( $user_id ) {
self::$memberships = new Memberships;
$membership = self::$memberships->findActiveByUserID( $user_id );
if ( empty( $membership ) ) {
return false;
}
return true;
}
public static function webhookSetup() {
$root = Routes::getAddress();
// $root = "https://stripe.joeykimsey.com/";
$response = self::$stripe->webhookEndpoints->create([
'enabled_events' => self::$webhookEvents,
'url' => $root . 'api/stripe/webhook',
]);
return $response;
}
// public static function webhookSetup() {
// $root = Routes::getAddress();
// $root = "https://stripe.joeykimsey.com/";
// $response = self::$stripe->webhookEndpoints->create([
// 'enabled_events' => self::$webhookEvents,
// 'url' => $root . 'api/stripe/webhook',
// ]);
// return $response;
// }
}

View File

@ -0,0 +1,44 @@
<legend>Memberships</legend>
{PAGINATION}
<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" role="button"><i class="glyphicon glyphicon-open"></i></a></td>
<td><a href="{ROOT_URL}admin/records/delete/{ID}" class="btn btn-sm btn-danger" role="button"><i class="glyphicon glyphicon-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">Delete</button>
</form>

View File

@ -0,0 +1,34 @@
<legend>Create Membership Product</legend>
<form action="" method="post" class="form-horizontal">
<input type="hidden" name="token" value="{TOKEN}">
<div class="form-group">
<label for="name" class="col-lg-3 control-label">Name</label>
<div class="col-lg-3">
<input type="text" class="form-control" name="name" id="name">
</div>
</div>
<div class="form-group">
<label for="description" class="col-lg-3 control-label">Description:</label>
<div class="col-lg-3">
<textarea class="form-control" name="description" maxlength="2000" rows="10" cols="50" id="description"></textarea>
</div>
</div>
<div class="form-group">
<label for="monthly_price" class="col-lg-3 control-label">Monthly Price:</label>
<div class="col-lg-3">
<input type="number" class="form-control" name="monthly_price" id="monthly_price">
</div>
</div>
<div class="form-group">
<label for="yearly_price" class="col-lg-3 control-label">Yearly Price:</label>
<div class="col-lg-3">
<input type="number" class="form-control" name="yearly_price" id="yearly_price">
</div>
</div>
<div class="form-group">
<label for="submit" class="col-lg-3 control-label"></label>
<div class="col-lg-3">
<button name="submit" value="submit" type="submit" class="btn btn-lg btn-primary center-block ">Submit</button>
</div>
</div>
</form>

View File

@ -0,0 +1,34 @@
<legend>Edit Membership Product</legend>
<form action="" method="post" class="form-horizontal">
<input type="hidden" name="token" value="{TOKEN}">
<div class="form-group">
<label for="name" class="col-lg-3 control-label">Name</label>
<div class="col-lg-3">
<input type="text" class="form-control" name="name" id="name" value="{name}">
</div>
</div>
<div class="form-group">
<label for="description" class="col-lg-3 control-label">Description:</label>
<div class="col-lg-3">
<textarea class="form-control" name="description" maxlength="2000" rows="10" cols="50" id="description">{description}</textarea>
</div>
</div>
<div class="form-group">
<label for="monthly_price" class="col-lg-3 control-label">Monthly Price:</label>
<div class="col-lg-3">
<input type="number" class="form-control" name="monthly_price" id="monthly_price" value="{monthly_price}">
</div>
</div>
<div class="form-group">
<label for="yearly_price" class="col-lg-3 control-label">Yearly Price:</label>
<div class="col-lg-3">
<input type="number" class="form-control" name="yearly_price" id="yearly_price" value="{yearly_price}">
</div>
</div>
<div class="form-group">
<label for="submit" class="col-lg-3 control-label"></label>
<div class="col-lg-3">
<button name="submit" value="submit" type="submit" class="btn btn-lg btn-primary center-block ">Submit</button>
</div>
</div>
</form>

View File

@ -0,0 +1,41 @@
<legend>Membership Products</legend>
{PAGINATION}
<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: 55%">Yearly Price</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" role="button"><i class="glyphicon glyphicon-open"></i></a></td>
<td><a href="{ROOT_URL}admin/products/delete/{ID}" class="btn btn-sm btn-danger" role="button"><i class="glyphicon glyphicon-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" role="button">Create</a>
<button name="submit" value="submit" type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>

View File

@ -0,0 +1,56 @@
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-6 col-lg-6 col-xs-offset-0 col-sm-offset-0 col-md-offset-3 col-lg-offset-3 top-pad" >
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Membership Product</h3>
</div>
<div class="panel-body">
<div class="row">
<div class=" col-md-12 col-lg-12 ">
<table class="table table-user-primary">
<tbody>
<tr>
<td align="left" width="200">ID:</td>
<td align="right">{ID}</td>
</tr>
<tr>
<td>Time submitted:</td>
<td align="right">{DTC}{time}{/DTC}</td>
</tr>
<tr>
<td>IP:</td>
<td align="right">{ip}</td>
</tr>
<tr>
<td>Email:</td>
<td align="right">{email}</td>
</tr>
<tr>
<td>Name</td>
<td align="right">{name}</td>
</tr>
<tr>
<td align="center" colspan="2">Feedback</td>
</tr>
<tr>
<td colspan="2">{feedback}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="panel-footer">
{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="glyphicon glyphicon-remove"></i></button>
</form>
{/ADMIN}
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,10 @@
<legend>WARNING: Regenerating existing webhooks makes joey sad, don't do iit!</legend>
<form action="" method="post" class="form-horizontal">
<div class="form-group">
<label for="submit" class="col-lg-3 control-label"></label>
<div class="col-lg-3">
<button name="submit" value="submit" type="submit" class="btn btn-lg btn-primary center-block ">Submit</button>
</div>
</div>
</form>

View File

@ -0,0 +1,21 @@
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2 text-center">
<h2>Are You Sure You Want to Cancel?</h2>
<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-6">
<a href="{ROOT_URL}member/cancelconfirm/{cancelid}" class="btn btn-lg btn-danger btn-block" role="button">Cancel Subscription</a>
</div>
<div class="col-md-6">
<a href="{ROOT_URL}member/manage" class="btn btn-lg btn-primary btn-block" role="button">Go Back</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,62 @@
<section id="features" class="container">
<div class="row">
<div class="col-md-4">
<h3>Organize Your Bookmarks</h3>
<p>Keep all your bookmarks organized with custom categories and tags.</p>
</div>
<div class="col-md-4">
<h3>Import and Export</h3>
<p>Seamlessly import and export your bookmarks (paid plans).</p>
</div>
<div class="col-md-4">
<h3>Accessible Anywhere</h3>
<p>Your bookmarks are securely stored and accessible from any device.</p>
</div>
</div>
</section>
<section id="pricing" class="text-center bg-light-gray" style="padding-top: 30px;">
<div class="container">
<h2>Pricing</h2>
<div class="row">
<div class="col-sm-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3>Free</h3>
</div>
<div class="panel-body">
<p>Basic bookmark storage</p>
<p>No import/export</p>
<p>Completely free</p>
<a href="{ROOT_URL}member/getmonthly/{ID}" class="btn btn-success" role="button">Sign Up</a>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="panel panel-primary">
<div class="panel-heading">
<h3>{name} Monthly</h3>
</div>
<div class="panel-body">
<p>Import/export bookmarks</p>
<p>Advanced curation tools</p>
<p>{prettyPriceMonthly}/month</p>
<a href="{ROOT_URL}member/getmonthly/{ID}" class="btn btn-primary" role="button">Sign Up</a>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="panel panel-primary">
<div class="panel-heading">
<h3>{name} Yearly</h3>
</div>
<div class="panel-body">
<p>Save with annual billing</p>
<p>All features included</p>
<p>{prettyPriceYearly}/year</p>
<a href="{ROOT_URL}member/getyearly/{ID}" class="btn btn-primary" role="button">Sign Up</a>
</div>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,38 @@
<section id="features" class="container">
<h2 class="text-center">Why Choose Us?</h2>
<div class="row">
<div class="col-sm-6">
<h3>Free Version</h3>
<p>Basic storage for all your bookmarks, forever free.</p>
</div>
<div class="col-sm-6">
<h3>Pro Features</h3>
<p>Unlock powerful import/export and advanced organization.</p>
</div>
</div>
</section>
<section id="pricing" class="container">
<h2 class="text-center">Affordable Plans</h2>
<div class="row">
<div class="col-sm-6">
<div class="panel panel-success">
<div class="panel-heading">{name} Monthly Plan</div>
<div class="panel-body">
<p>{prettyPriceMonthly}/month</p>
<p>All pro features unlocked</p>
<a href="{ROOT_URL}member/getmonthly/{ID}" class="btn btn-success btn-block" role="button">Get Started</a>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="panel panel-info">
<div class="panel-heading">{name} Yearly Plan</div>
<div class="panel-body">
<p>{prettyPriceYearly}/year</p>
<p>Save {prettySavings} annually!</p>
<a href="{ROOT_URL}member/getyearly/{ID}" class="btn btn-info btn-block" role="button">Sign Up</a>
</div>
</div>
</div>
</div>
</section>

View File

@ -0,0 +1,35 @@
<legend>Memberships</legend>
<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>
</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-primary" role="button"><i class="glyphicon glyphicon-pause"></i></a></td>
<td><a href="{ROOT_URL}member/cancel/{ID}" class="btn btn-sm btn-danger" role="button"><i class="glyphicon glyphicon-remove"></i></a></td>
</tr>
{/LOOP}
{ALT}
<tr>
<td align="center" colspan="6">
No results to show.
</td>
</tr>
{/ALT}
</tbody>
</table>
<a href="{ROOT_URL}member/managepayment" class="btn btn-sm btn-primary" role="button">Manage Payment Method</a>

View File

@ -0,0 +1,21 @@
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2 text-center">
<h2>Are You Sure You Want to Pause?</h2>
<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-6">
<a href="{ROOT_URL}member/pauseconfirm/{pauseid}" class="btn btn-lg btn-danger btn-block" role="button">Pause Subscription</a>
</div>
<div class="col-md-6">
<a href="{ROOT_URL}member/manage" class="btn btn-lg btn-primary btn-block" role="button">Go Back</a>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,4 @@
<div class="jumbotron text-center">
<h1>Take your time, its not a sprint, its a marathon.</h1>
<p>Its ok, no-one wants to checkout too fast, its embarrassing.</p>
</div>

View File

@ -0,0 +1,5 @@
<div class="jumbotron text-center">
<h1>Thanks for joining!</h1>
<p>Its people like you who keep the pixels on around here.
Its people like me who tell you life is unfair and it could take up to an hour for your membership to activate.</p>
</div>

View File

@ -0,0 +1,27 @@
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2 text-center">
<h2>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>X%</strong> compared to the monthly plan.
Stay committed and make the most of our service.
</p>
<div class="row">
<div class="col-md-6">
<button class="btn btn-lg btn-success btn-block">
Upgrade to Yearly
</button>
</div>
<div class="col-md-6">
<button class="btn btn-lg btn-primary btn-block">
Stay on Monthly
</button>
</div>
</div>
</div>
</div>
</div>