diff --git a/app/plugins/bookmarks/models/bookmarks.php b/app/plugins/bookmarks/models/bookmarks.php index 726e4a6..39b77f3 100644 --- a/app/plugins/bookmarks/models/bookmarks.php +++ b/app/plugins/bookmarks/models/bookmarks.php @@ -12,9 +12,8 @@ */ namespace TheTempusProject\Models; -use TheTempusProject\Bedrock\Classes\Config; use TheTempusProject\Bedrock\Functions\Check; -use TheTempusProject\Canary\Canary as Debug; +use TheTempusProject\Canary\Bin\Canary as Debug; use TheTempusProject\Classes\DatabaseModel; use TheTempusProject\TheTempusProject as App; use TheTempusProject\Houdini\Classes\Filters; diff --git a/app/plugins/bookmarks/models/bookmarkviews.php b/app/plugins/bookmarks/models/bookmarkviews.php index e25d50f..624b336 100644 --- a/app/plugins/bookmarks/models/bookmarkviews.php +++ b/app/plugins/bookmarks/models/bookmarkviews.php @@ -12,7 +12,6 @@ */ namespace TheTempusProject\Models; -use TheTempusProject\Bedrock\Classes\Config; use TheTempusProject\Bedrock\Functions\Check; use TheTempusProject\Canary\Bin\Canary as Debug; use TheTempusProject\Classes\DatabaseModel; diff --git a/app/plugins/bookmarks/models/folders.php b/app/plugins/bookmarks/models/folders.php index 218f6c2..62a6367 100644 --- a/app/plugins/bookmarks/models/folders.php +++ b/app/plugins/bookmarks/models/folders.php @@ -12,9 +12,8 @@ */ namespace TheTempusProject\Models; -use TheTempusProject\Bedrock\Classes\Config; use TheTempusProject\Bedrock\Functions\Check; -use TheTempusProject\Canary\Canary as Debug; +use TheTempusProject\Canary\Bin\Canary as Debug; use TheTempusProject\Classes\DatabaseModel; use TheTempusProject\TheTempusProject as App; use TheTempusProject\Houdini\Classes\Filters; diff --git a/app/plugins/donate/controllers/donate.php b/app/plugins/donate/controllers/donate.php new file mode 100644 index 0000000..e6b2267 --- /dev/null +++ b/app/plugins/donate/controllers/donate.php @@ -0,0 +1,84 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Controllers; + +use TheTempusProject\Hermes\Functions\Redirect; +use TheTempusProject\Bedrock\Functions\Check; +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Bedrock\Functions\Session; +use TheTempusProject\Houdini\Classes\Issues; +use TheTempusProject\Houdini\Classes\Views; +use TheTempusProject\Classes\Controller; +use TheTempusProject\Classes\Forms; +use TheTempusProject\Models\Bugreport as BugreportModel; +use TheTempusProject\TheTempusProject as App; +use TheTempusProject\Bedrock\Classes\Config; +use TheTempusProject\Hermes\Functions\Route as Routes; + +class Donate extends Controller { + protected static $bugreport; + + public function index() { + self::$title = 'Donate - {SITENAME}'; + self::$pageDescription = 'Donations help support our cause and our efforts. Please use this page to select an amount and give today.'; + + if ( !Input::exists() ) { + return Views::view( 'donate.donate' ); + } + + if ( !Forms::check( 'donate' ) ) { + Issues::add( 'error', [ 'There was an error with your form.' => Check::userErrors() ] ); + return Views::view( 'donate.donate' ); + } + + try { + $api_key = Config::getValue( 'donations/stripeSecret' ); + $stripe = new \Stripe\StripeClient( $api_key ); + $session = $stripe->checkout->sessions->create([ + 'payment_method_types' => ['card'], + 'line_items' => [[ + 'price_data' => [ + 'currency' => 'usd', + 'product_data' => [ + 'name' => 'Donation', + ], + 'unit_amount' => Input::post("amount"), // Amount in cents ($10) + ], + 'quantity' => 1, + ]], + 'mode' => 'payment', + 'metadata' => [ + 'note' => Input::post("comment"), + ], + 'success_url' => Routes::getAddress() . 'donate/success?session_id={CHECKOUT_SESSION_ID}', + 'cancel_url' => Routes::getAddress() . 'donate/continue', + ]); + + // Redirect to the Checkout session + header('Location: ' . $session->url); + exit; + } catch (\Stripe\Exception\ApiErrorException $e) { + echo "Error creating Checkout session: " . $e->getMessage(); + } + } + + public function success() { + self::$title = 'Thank you!'; + Views::view( 'donate.success' ); + } + + public function continue() { + self::$title = 'Thank you!'; + Views::view( 'donate.fail' ); + } +} diff --git a/app/plugins/donate/forms.php b/app/plugins/donate/forms.php new file mode 100644 index 0000000..7f9e1a9 --- /dev/null +++ b/app/plugins/donate/forms.php @@ -0,0 +1,43 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Plugins\Donate; + +use TheTempusProject\Bedrock\Functions\Input; +use TheTempusProject\Classes\Forms; + +class DonateForms extends Forms { + /** + * Adds these functions to the form list. + */ + public function __construct() { + self::addHandler( 'donate', __CLASS__, 'donate' ); + } + + /** + * Validates the bug report form. + * + * @return {bool} + */ + public static function donate() { + if ( !Input::exists( 'amount' ) ) { + self::addUserError( 'You must specify an amount' ); + return false; + } + if ( !self::token() ) { + return false; + } + return true; + } +} + +new DonateForms; diff --git a/app/plugins/donate/models/donations.php b/app/plugins/donate/models/donations.php new file mode 100644 index 0000000..391ebdf --- /dev/null +++ b/app/plugins/donate/models/donations.php @@ -0,0 +1,35 @@ + + * @link https://TheTempusProject.com + * @license https://opensource.org/licenses/MIT [MIT LICENSE] + */ +namespace TheTempusProject\Models; + +use TheTempusProject\Classes\DatabaseModel; +use TheTempusProject\Plugins\Bugreport as Plugin; + +class Donations extends DatabaseModel { + public $tableName = 'donations'; + public $databaseMatrix = [ + [ 'amount', 'int', '11' ], + [ 'createdAt', 'int', '10' ], + [ 'user_d', 'int', '10' ], + [ 'comment', 'text', '' ], + ]; + public $plugin; + + /** + * The model constructor. + */ + public function __construct() { + parent::__construct(); + $this->plugin = new Plugin; + } +} diff --git a/app/plugins/donate/plugin.php b/app/plugins/donate/plugin.php new file mode 100644 index 0000000..2e6a48d --- /dev/null +++ b/app/plugins/donate/plugin.php @@ -0,0 +1,60 @@ + + * @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; + +class Donate extends Plugin { + public static $stripe; + private static $loaded = false; + public $pluginName = 'TP Donations'; + public $configName = 'donations'; + 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 donation system.'; + public $permissionMatrix = [ + 'controlMemberships' => [ + 'pretty' => 'Can donate.', + 'default' => true, + ], + ]; + public $main_links = [ + [ + 'text' => 'Donate!', + 'url' => '{ROOT_URL}donate', + ], + ]; + public $configMatrix = [ + 'stripePublishable' => [ + 'type' => 'text', + 'pretty' => 'Stripe Publishable key', + 'default' => 'pk_xxxxxxxxxxxxxxx', + ], + 'stripeSecret' => [ + 'type' => 'text', + 'pretty' => 'Stripe Secret key', + 'default' => 'sk_xxxxxxxxxxxxxxx', + ], + ]; + + public function __construct( $load = false ) { + parent::__construct( $load ); + } +} diff --git a/app/plugins/donate/views/donate.html b/app/plugins/donate/views/donate.html new file mode 100644 index 0000000..69f6e58 --- /dev/null +++ b/app/plugins/donate/views/donate.html @@ -0,0 +1,37 @@ +
+
+
+
+

Support Our Projects

+

Your funding helps provide more time and resources, enables exciting new projects, and ensures continued updates and improvements to all of our services.

+

By contributing, you're supporting these platforms:

+
    +
  • AllTheBookmarks.com - Your one-stop bookmark management service.
  • +
  • ForgottenPlanner.com - Keep your plans on track.
  • +
  • TempusToolkit.com - Time management made easier.
  • +
  • WoWditions.com - Enhancing your World of Warcraft experience.
  • +
  • WhatsThatUploadSite.com - Simplified upload site discovery.
  • +
  • PollingDownTheStreet.com - Find your local polling station effortlessly.
  • +
+

Every donation helps us grow and continue providing value to our community. Thank you for your support!

+
+
+
+ +
+
+
+
+ + +
+
+ + +
+ + +
+
+
+
\ No newline at end of file diff --git a/app/plugins/donate/views/fail.html b/app/plugins/donate/views/fail.html new file mode 100644 index 0000000..c32e583 --- /dev/null +++ b/app/plugins/donate/views/fail.html @@ -0,0 +1,4 @@ +
+

Thanks for taking the time to explore donation.

+

Now may not be the right time, but we thank you anyways!

+
\ No newline at end of file diff --git a/app/plugins/donate/views/success.html b/app/plugins/donate/views/success.html new file mode 100644 index 0000000..458ea88 --- /dev/null +++ b/app/plugins/donate/views/success.html @@ -0,0 +1,5 @@ +
+

Thanks for supporting!

+

Its people like you who keep the pixels on around here. + Every Dollar counts, so thank you again.

+
\ No newline at end of file diff --git a/app/plugins/members/controllers/admin/invoices.php b/app/plugins/members/controllers/admin/invoices.php new file mode 100644 index 0000000..7d5baba --- /dev/null +++ b/app/plugins/members/controllers/admin/invoices.php @@ -0,0 +1,50 @@ + + * @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 ) { + } +} diff --git a/app/plugins/members/controllers/admin/members.php b/app/plugins/members/controllers/admin/members.php new file mode 100644 index 0000000..53dd9ef --- /dev/null +++ b/app/plugins/members/controllers/admin/members.php @@ -0,0 +1,40 @@ + + * @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!' ); + } +} diff --git a/app/plugins/members/controllers/admin/products.php b/app/plugins/members/controllers/admin/products.php new file mode 100644 index 0000000..30bdfbc --- /dev/null +++ b/app/plugins/members/controllers/admin/products.php @@ -0,0 +1,97 @@ + + * @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(); + } +} diff --git a/app/plugins/members/controllers/admin/records.php b/app/plugins/members/controllers/admin/records.php new file mode 100644 index 0000000..069fe2f --- /dev/null +++ b/app/plugins/members/controllers/admin/records.php @@ -0,0 +1,50 @@ + + * @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 ) { + } +} diff --git a/app/plugins/members/controllers/api/stripe.php b/app/plugins/members/controllers/api/stripe.php new file mode 100644 index 0000000..4f08745 --- /dev/null +++ b/app/plugins/members/controllers/api/stripe.php @@ -0,0 +1,188 @@ + + * @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 )]); + } +} \ No newline at end of file diff --git a/app/plugins/members/controllers/member.php b/app/plugins/members/controllers/member.php index a43366f..8a800d9 100644 --- a/app/plugins/members/controllers/member.php +++ b/app/plugins/members/controllers/member.php @@ -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' ); + } +} \ No newline at end of file diff --git a/app/plugins/members/controllers/stripe_api_controller.php b/app/plugins/members/controllers/stripe_api_controller.php new file mode 100644 index 0000000..1e8a4ad --- /dev/null +++ b/app/plugins/members/controllers/stripe_api_controller.php @@ -0,0 +1,33 @@ + + * @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' ); + } +} diff --git a/app/plugins/members/forms.php b/app/plugins/members/forms.php new file mode 100644 index 0000000..295ea2c --- /dev/null +++ b/app/plugins/members/forms.php @@ -0,0 +1,47 @@ + + * @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; diff --git a/app/plugins/members/models/membership_customers.php b/app/plugins/members/models/membership_customers.php new file mode 100644 index 0000000..f2746e9 --- /dev/null +++ b/app/plugins/members/models/membership_customers.php @@ -0,0 +1,104 @@ + + * @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; + } + } +} \ No newline at end of file diff --git a/app/plugins/members/models/membership_invoices.php b/app/plugins/members/models/membership_invoices.php new file mode 100644 index 0000000..9412be8 --- /dev/null +++ b/app/plugins/members/models/membership_invoices.php @@ -0,0 +1,38 @@ + + * @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(); + } +} \ No newline at end of file diff --git a/app/plugins/members/models/membership_products.php b/app/plugins/members/models/membership_products.php new file mode 100644 index 0000000..45375b2 --- /dev/null +++ b/app/plugins/members/models/membership_products.php @@ -0,0 +1,206 @@ + + * @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 = '$' . number_format( $instance->monthly_price / 100, 2 ); // Outputs: $99.49 + $instance->prettyPriceYearly = '$' . number_format( $instance->yearly_price / 100, 2 ); // Outputs: $99.49 + $instance->prettySavings = '$' . number_format( + ( $instance->yearly_price - ( $instance->monthly_price * 12 ) ) / 100, + 2 + ); // Outputs: $99.49 + + $out[] = $instance; + if ( !empty( $end ) ) { + $out = $out[0]; + break; + } + } + return $out; + } + + public function findByPriceID( $d ) { + $data = self::$db->get( $this->tableName, [ 'stripe_price_monthly', '=', $d, 'OR', 'stripe_price_yearly', '=', $d ] ); + + if ( ! $data->count() ) { + return false; + } + return $data->first(); + } +} \ No newline at end of file diff --git a/app/plugins/members/models/memberships.php b/app/plugins/members/models/memberships.php new file mode 100644 index 0000000..c527303 --- /dev/null +++ b/app/plugins/members/models/memberships.php @@ -0,0 +1,170 @@ + + * @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 = '$' . 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; + } +} \ No newline at end of file diff --git a/app/plugins/members/plugin.php b/app/plugins/members/plugin.php index 1b52d5b..11c4f8b 100644 --- a/app/plugins/members/plugin.php +++ b/app/plugins/members/plugin.php @@ -1,10 +1,10 @@ * @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' => ' Memberships', - 'url' => '{ROOT_URL}admin/member', + 'text' => ' Memberships', + 'url' => [ + [ + 'text' => ' Products', + 'url' => '{ROOT_URL}admin/products', + ], + [ + 'text' => ' Subscriptions', + 'url' => '{ROOT_URL}admin/records', + ], + [ + 'text' => ' 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; + // } } diff --git a/app/plugins/members/views/admin/memberships/list.html b/app/plugins/members/views/admin/memberships/list.html new file mode 100644 index 0000000..981fe33 --- /dev/null +++ b/app/plugins/members/views/admin/memberships/list.html @@ -0,0 +1,44 @@ +Memberships +{PAGINATION} +
+ + + + + + + + + + + + + + + {LOOP} + + + + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
NamestatusPriceStartEnd + +
{name}{status}{prettyPrice}{DTC}{current_period_start}{/DTC}{DTC}{current_period_end}{/DTC} + +
+ No results to show. +
+ +
\ No newline at end of file diff --git a/app/plugins/members/views/admin/products/create.html b/app/plugins/members/views/admin/products/create.html new file mode 100644 index 0000000..c14a158 --- /dev/null +++ b/app/plugins/members/views/admin/products/create.html @@ -0,0 +1,34 @@ +Create Membership Product +
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/app/plugins/members/views/admin/products/edit.html b/app/plugins/members/views/admin/products/edit.html new file mode 100644 index 0000000..e211248 --- /dev/null +++ b/app/plugins/members/views/admin/products/edit.html @@ -0,0 +1,34 @@ +Edit Membership Product +
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/app/plugins/members/views/admin/products/list.html b/app/plugins/members/views/admin/products/list.html new file mode 100644 index 0000000..fe4f4af --- /dev/null +++ b/app/plugins/members/views/admin/products/list.html @@ -0,0 +1,41 @@ +Membership Products +{PAGINATION} +
+ + + + + + + + + + + + + {LOOP} + + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
NameMonthly PriceYearly Price + +
{name}{monthly_price}{yearly_price} + +
+ No results to show. +
+ Create + +
\ No newline at end of file diff --git a/app/plugins/members/views/admin/products/view.html b/app/plugins/members/views/admin/products/view.html new file mode 100644 index 0000000..536842d --- /dev/null +++ b/app/plugins/members/views/admin/products/view.html @@ -0,0 +1,56 @@ +
+
+
+
+
+

Membership Product

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID:{ID}
Time submitted:{DTC}{time}{/DTC}
IP:{ip}
Email:{email}
Name{name}
Feedback
{feedback}
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/app/plugins/members/views/admin/webhooks.html b/app/plugins/members/views/admin/webhooks.html new file mode 100644 index 0000000..d895635 --- /dev/null +++ b/app/plugins/members/views/admin/webhooks.html @@ -0,0 +1,10 @@ +WARNING: Regenerating existing webhooks makes joey sad, don't do iit! +
+
+ +
+ + +
+
+
\ No newline at end of file diff --git a/app/plugins/members/views/cancel.html b/app/plugins/members/views/cancel.html new file mode 100644 index 0000000..579b488 --- /dev/null +++ b/app/plugins/members/views/cancel.html @@ -0,0 +1,21 @@ +
+
+
+

Are You Sure You Want to Cancel?

+

+ Cancelling your subscription means you'll miss out on exclusive features, updates, and benefits. +

+

+ Consider staying to continue enjoying the full experience of our service. +

+ +
+
+
\ No newline at end of file diff --git a/app/plugins/members/views/landing1.html b/app/plugins/members/views/landing1.html new file mode 100644 index 0000000..8399ef3 --- /dev/null +++ b/app/plugins/members/views/landing1.html @@ -0,0 +1,62 @@ +
+
+
+

Organize Your Bookmarks

+

Keep all your bookmarks organized with custom categories and tags.

+
+
+

Import and Export

+

Seamlessly import and export your bookmarks (paid plans).

+
+
+

Accessible Anywhere

+

Your bookmarks are securely stored and accessible from any device.

+
+
+
+
+
+

Pricing

+
+
+
+
+

Free

+
+
+

Basic bookmark storage

+

No import/export

+

Completely free

+ Sign Up +
+
+
+
+
+
+

{name} Monthly

+
+
+

Import/export bookmarks

+

Advanced curation tools

+

{prettyPriceMonthly}/month

+ Sign Up +
+
+
+
+
+
+

{name} Yearly

+
+
+

Save with annual billing

+

All features included

+

{prettyPriceYearly}/year

+ Sign Up +
+
+
+
+
+
\ No newline at end of file diff --git a/app/plugins/members/views/landing2.html b/app/plugins/members/views/landing2.html new file mode 100644 index 0000000..67ffce3 --- /dev/null +++ b/app/plugins/members/views/landing2.html @@ -0,0 +1,38 @@ +
+

Why Choose Us?

+
+
+

Free Version

+

Basic storage for all your bookmarks, forever free.

+
+
+

Pro Features

+

Unlock powerful import/export and advanced organization.

+
+
+
+
+

Affordable Plans

+
+
+
+
{name} Monthly Plan
+
+

{prettyPriceMonthly}/month

+

All pro features unlocked

+ Get Started +
+
+
+
+
+
{name} Yearly Plan
+
+

{prettyPriceYearly}/year

+

Save {prettySavings} annually!

+ Sign Up +
+
+
+
+
\ No newline at end of file diff --git a/app/plugins/members/views/manage.html b/app/plugins/members/views/manage.html new file mode 100644 index 0000000..3b7a243 --- /dev/null +++ b/app/plugins/members/views/manage.html @@ -0,0 +1,35 @@ +Memberships + + + + + + + + + + + + + + {LOOP} + + + + + + + + + + {/LOOP} + {ALT} + + + + {/ALT} + +
NamestatusPriceStartEnd
{productName}{status}{prettyPrice}{DTC=date}{current_period_start}{/DTC}{DTC=date}{current_period_end}{/DTC}
+ No results to show. +
+Manage Payment Method \ No newline at end of file diff --git a/app/plugins/members/views/pause.html b/app/plugins/members/views/pause.html new file mode 100644 index 0000000..ec667e1 --- /dev/null +++ b/app/plugins/members/views/pause.html @@ -0,0 +1,21 @@ +
+
+
+

Are You Sure You Want to Pause?

+

+ Pausing your subscription means you'll miss out on exclusive features, updates, and benefits. +

+

+ Consider staying to continue enjoying the full experience of our service. +

+
+ +
+ Go Back +
+
+
+
+
\ No newline at end of file diff --git a/app/plugins/members/views/paymentcanceled.html b/app/plugins/members/views/paymentcanceled.html new file mode 100644 index 0000000..ec98c3c --- /dev/null +++ b/app/plugins/members/views/paymentcanceled.html @@ -0,0 +1,4 @@ +
+

Take your time, its not a sprint, its a marathon.

+

Its ok, no-one wants to checkout too fast, its embarrassing.

+
\ No newline at end of file diff --git a/app/plugins/members/views/paymentcomplete.html b/app/plugins/members/views/paymentcomplete.html new file mode 100644 index 0000000..962ac77 --- /dev/null +++ b/app/plugins/members/views/paymentcomplete.html @@ -0,0 +1,5 @@ +
+

Thanks for joining!

+

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.

+
\ No newline at end of file diff --git a/app/plugins/members/views/upgrade.html b/app/plugins/members/views/upgrade.html new file mode 100644 index 0000000..f28404e --- /dev/null +++ b/app/plugins/members/views/upgrade.html @@ -0,0 +1,27 @@ +
+
+
+

Upgrade to a Yearly Plan

+

+ Save more and enjoy uninterrupted access to all features with our yearly plan! +

+

+ Upgrading now means you'll save X% compared to the monthly plan. + Stay committed and make the most of our service. +

+
+
+ +
+
+ +
+
+
+
+
+ \ No newline at end of file diff --git a/composer.json b/composer.json index 38180ab..f1868db 100644 --- a/composer.json +++ b/composer.json @@ -20,11 +20,12 @@ ], "require": { + "components/jquery": "1.9.*", "fortawesome/font-awesome": "4.7", + "stripe/stripe-php": "^16.3", "thetempusproject/bedrock": "1.0.10", "thetempusproject/canary": "1.0.5", "thetempusproject/houdini": "1.0.8", - "components/jquery": "1.9.*", "twbs/bootstrap": "3.3.7" }, "autoload": diff --git a/composer.lock b/composer.lock index 7060596..f0820a1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4d87b4a533236913cbbc0e75664016de", + "content-hash": "017241adb07073e9ec4d3fa64ae5a66c", "packages": [ { "name": "components/jquery", @@ -240,6 +240,65 @@ "abandoned": "oomphinc/composer-installers-extender", "time": "2013-08-31T23:46:48+00:00" }, + { + "name": "stripe/stripe-php", + "version": "v16.3.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "48af6bc64ca8157b3fdce100e856069963bac466" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/48af6bc64ca8157b3fdce100e856069963bac466", + "reference": "48af6bc64ca8157b3fdce100e856069963bac466", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.5.0", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "support": { + "issues": "https://github.com/stripe/stripe-php/issues", + "source": "https://github.com/stripe/stripe-php/tree/v16.3.0" + }, + "time": "2024-11-20T23:30:16+00:00" + }, { "name": "symfony/process", "version": "v3.4.47", @@ -540,5 +599,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" }