all atb changes

This commit is contained in:
Joey Kimsey
2025-02-05 23:57:17 -05:00
parent 2ac64e5c49
commit ffb82b1192
328 changed files with 12384 additions and 2477 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,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');
}
}

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

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

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\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 )]);
}
}

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

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

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,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 = '&#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 $this->filter( $data->first() );
}
public function mainProduct() {
$data = self::$db->get( $this->tableName, '*' );
if ( ! $data->count() ) {
return false;
}
return $this->filter( $data->first() );
}
}

View 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 = '&#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->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;
}
}

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

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

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

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

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

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

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

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

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

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

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

View 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">&#36;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>

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

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

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

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

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

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

View 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 &#36;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>

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