Okay, here is my registration adapter. Please note that I've called it "joomlastaq" (because my LLC is "Sleestaq" so I postfix most of my name spaces with "staq" :-))
This will require the appropriate entry in wireframe for the registration button, of course.
<?php
/**
* @package Sleestaq Joomla Registration Adapter for PayPlans
* @copyright Copyright (C) 2020 Sleestaq, LLC. All rights reserved.
* @license GNU/GPL, see LICENSE.php
* @version 1.0.0
*
* PayPlans is free software. This version may have been modified pursuant
* to the GNU General Public License, and as distributed it includes or
* is derivative of works licensed under the GNU General Public License or
* other free or open source software licenses.
*
* See COPYRIGHT.php for copyright notices and details.
*/
use Joomla\Registry\Registry;
defined('_JEXEC') or die('Unauthorized Access');
class PPRegistrationJoomlastaq extends PayPlans
{
public string $type = 'joomlastaq';
public string $url;
private array $templateVars = array();
private string $sessionKey;
protected PPSession $session;
/**
* PPRegistrationJoomlaStaq constructor.
*
* @noinspection PhpUndefinedMethodInspection (session is called magically)
*
* @throws Exception
*
* @since 1.0.0
*/
public function __construct()
{
$urlBuild = JRoute::_('index.php?option=com_users&view=registration');
if (!$urlBuild) {
throw new Exception("JRoute Failure!");
}
$this->url = $urlBuild;
$this->session = PP::session();
parent::__construct();
}
/**
* Build the URL to hit the registration endpoint. Note that we add the plan_id and invoice_key to
* the URL. This lets us check for them in onAfterRoute and use them exclusively. If there is anything
* in the session object but not on the query string, we presume that the user is trying to hit the
* registration endpoint directly, and we don't like that.
*
* This function is called by our html() method which, in turn, is called by the theme when it wants to
* create a link or button to the Joomla registration component. The register view is going to call back
* into here to set the plan ID into the session and then call our redirect() function. Our redirect function
* will then call into Joomla's registration endpoint with the Plan ID and Invoice Key on the query string and,
* as outlined above, trigger our onAfterRoute to verify that this is, indeed, the case.
*
* To summarize:
*
* 1. Call Below creates a URL sufficient to call com_payplans/register with plan ID and invoice key
* on query string
* 2. That view then uses our adapter to call into Joomla's registration interface and
* 3. Our onAfterRoute validates the query string is intact
*
* @param PPInvoice $invoice
*
* @return array|string|string[]|null
*
* @since 1.0.0
*/
protected function getRegistrationUrl(PPInvoice $invoice)
{
$plan = $invoice->getPlan();
return PPR::_('index.php?option=com_payplans&view=register&plan_id=' . $plan->getId() . '&invoice_key=' . $invoice->getKey());
}
/**
* @param PPInvoice $invoice
*
* @return string
*
* @since 1.0.0
*/
public function html(PPInvoice $invoice): string
{
$this->set('url', $this->getRegistrationUrl($invoice));
$this->set('userId', $this->getNewUserId());
return $this->display('default');
}
/**
* Determines if this is currently the registration url.
*
* For Joomla, that's com_users and the registration view with
* no task. The "no task" part is important because we don't want
* to fire on the "complete" (or any other) task.
*
* @return bool
*
* @since 1.0.0
*/
protected function isRegistrationUrl(): bool
{
$option = (string)$this->input->get('option', '', 'default');
$view = (string)$this->input->get('view', '', 'default');
$task = (string)$this->input->get('task', '', 'default');
return ($option === 'com_users' && $view === 'registration' && !$task);
}
/**
* Provides assistance to the payment app to output contents.
* This is similar to the display method in views
*
* @return string
*
* @var string $namespace
*
* @since 1.0.0
*
* @noinspection PhpUndefinedMethodInspection (themes called magically)
*/
public function display(string $namespace = 'default'): string
{
$this->processData();
$namespace = 'site/registration/' . $this->type . '/' . $namespace;
$theme = PP::themes();
$theme->setVars($this->templateVars);
return $theme->output($namespace);
}
/**
* Retrieves the com_user's global configuration
*
* @return Registry
*
* @since 1.0.0
*/
protected function getJoomlaUsersParams(): Registry
{
static $params = null;
if (is_null($params)) {
$params = JComponentHelper::getParams('com_users');
}
return $params;
}
/**
* Resets the session data. Note that we only want to reset the invoice key and the
* plan ID. If we have set a user ID, we want that sticking around.
*
* @return void
*
* @since 1.0.0
*/
protected function resetSessionVars(): void
{
$this->session->set('PP_INVOICE_KEY', '');
$this->session->set('REGISTRATION_PLAN_ID', 0);
$this->session->set('REGISTRATION_EXPIRE', 0);
}
/**
* @param $id
*
* @return void
*
* @since 1.0.0
*/
public function setNewUserId($id): void
{
$this->session->set('REGISTRATION_NEW_USER_ID', $id);
}
/**
* @return int
*
* @since 1.0.0
*/
public function getNewUserId(): int
{
return (int)$this->session->get('REGISTRATION_NEW_USER_ID', 0);
}
/**
* The idea here is that when the user starts the process and the invoice key and plan ID are
* put in the session, they have ten minutes (600 seconds) to complete the process. If they leave
* and come back after ten minutes, they will be treated as new
*
* @var int
*
* @since 1.0.0
*/
protected static int $expireTimeInSeconds = 600;
/**
* Sets the invoice key in session so it can be tracked later
*
* @param string $key
*
* @return void
*
* @since 1.0.0
*/
public function setInvoiceKey(string $key): void
{
$this->session->set('PP_INVOICE_KEY', $key);
$this->session->set('REGISTRATION_EXPIRE', time() + self::$expireTimeInSeconds);
}
/**
* Retrieves the invoice id from the query or session
*
* @param bool $returnAnyway
*
* @return false|int
*
* @since 1.0.0
*/
public function getInvoiceId(bool $returnAnyway)
{
$key = $this->input->get('invoice_key', '', 'default');
if (empty($key)) {
$key = $this->session->get('PP_INVOICE_KEY', '');
if (!empty($key)) {
$expire = (int)$this->session->get('REGISTRATION_EXPIRE', 0);
if ($expire < time()) {
// There was a session variable, but it's since expired. So we clear
// it out and act as if it wasn't there.
$this->resetSessionVars();
$key = $returnAnyway ? $key : '';
}
}
}
if (empty($key)) {
return false;
}
return PP::getIdFromKey($key);
}
/**
* Sets the plan id into the session so that it can be tracked later
*
* @param int $planId
*
* @return void
*
* @since 1.0.0
*/
public function setPlanId(int $planId): void
{
$this->session->set('REGISTRATION_PLAN_ID', $planId);
$this->session->set('REGISTRATION_EXPIRE', time() + self::$expireTimeInSeconds);
}
/**
* Retrieves the plan id from the query or session
*
* @return int
*
* @since 1.0.0
*/
public function getPlanId(): int
{
$planId = (int)$this->input->get('plan_id', 0, 'int');
if ($planId === 0) {
$planId = (int)$this->session->get('REGISTRATION_PLAN_ID', 0);
if ($planId) {
$expire = (int)$this->session->get('REGISTRATION_EXPIRE', 0);
if ($expire < time()) {
// There was a session variable, but it's since expired. So we clear
// it out and act as if it wasn't there.
$this->resetSessionVars();
$planId = 00;
}
}
}
return $planId;
}
/**
* Determines whether need to update the current new registering user status
* Implemented by child
*
* @param $invoice
* @param $userId
*
* @return void
*
* @since 1.0.0
*/
protected function updateNewUserPublishState($invoice, $userId): void
{
// Do Nothing
}
/**
* Set the session key
*
* @param $key
*
* @return void
*
* @since 1.0.0
*/
public function setSessionKey($key): void
{
$this->sessionKey = $key;
}
/**
* Set the data stored from the previous session
*
* @return void
*
* @since 1.0.0
*/
public function processData(): void
{
$default = array(
'register_name' => '',
'register_username' => '',
'register_email' => '',
'address' => '',
'city' => '',
'state' => '',
'zip' => '',
'country' => ''
);
// Get data from previous session
$data = $this->session->get($this->sessionKey, '', 'payplans', true);
if ($data) {
$mapping = array('name', 'username', 'email', 'password', 'password2');
foreach ($mapping as $key) {
if (isset($data[$key])) {
// We do not want to display the password back to the form
if ($key === 'password' || $key === 'password2') {
unset($data[$key]);
continue;
}
$newKey = 'register_' . $key;
$data[$newKey] = $data[$key];
unset($data[$key]);
}
}
} else {
$data = $default;
}
$this->set('data', $data);
}
/**
* Provides assistance to the payment app to set variables which can be extracted with the @display method
*
* @param $key
* @param $value
*
* @return void
*
* @since 1.0.0
*/
public function set($key, $value): void
{
$this->templateVars[$key] = $value;
}
/**
* Redirects the request to the respective registration page
*
* @return void
*
* @since 1.0.0
*/
public function redirect(): void
{
$invoice_key = $this->input->get('invoice_key', 0, 'default');
$plan_id = $this->input->get('plan_id', 0, 'int');
$redirectTo = $this->url . "?plan_id=$plan_id&invoice_key=$invoice_key";
$this->app->redirect($redirectTo);
$this->app->close();
}
/**
* Redirects user to the checkout page
*
* @param $id
*
* @return void
*
* @since 1.0.0
*
* @noinspection PhpUndefinedMethodInspection (invoice called magically)
*/
public function redirectToCheckout($id): void
{
$invoice = PP::invoice($id);
$invoiceKey = $invoice->getKey();
// Directly go to thanks page for free invoice
if ($this->config->get('skip_free_invoices') && $invoice->isFree()) {
$redirect = PPR::_('index.php?option=com_payplans&task=checkout.confirm&invoice_key=' . $invoiceKey . '&app_id=0', false);
$this->app->redirect($redirect);
} else {
$redirect = PPR::_('index.php?option=com_payplans&view=checkout&invoice_key=' . $invoiceKey, false);
$this->app->redirect($redirect);
}
$this->app->close();
}
/**
* Redirects user to the plan page
*
* @return void
*
* @since 1.0.0
*/
public function redirectToPlans(): void
{
$redirect = PPR::_('index.php?option=com_payplans&view=plan', false);
$this->app->redirect($redirect);
$this->app->close();
}
/**
* Determines if the registration requires activation
*
* @return bool
*
* @since 1.0.0
*/
public function requireActivation(): bool
{
$usersConfig = $this->getJoomlaUsersParams();
$registrationType = (int)$usersConfig->get('useractivation');
return $registrationType === 1;
}
/**
* Determines if the registration requires admin for approval
*
* @return bool
*
* @since 1.0.0
*/
public function requireAdminActivation(): bool
{
$usersConfig = $this->getJoomlaUsersParams();
$registrationType = (int)$usersConfig->get('useractivation');
return $registrationType === 2;
}
/**
* Determines if the registration requires activation
*
* @return bool
*
* @since 1.0.0
*/
public function requireUserActivation(): bool
{
$usersConfig = $this->getJoomlaUsersParams();
$registrationType = (int)$usersConfig->get('useractivation');
return $registrationType === 1;
}
// TRIGGERS
//
// Methods called from external forces
/**
* Triggered by registration plugin
*
* @return bool
*
* @since 1.0.0
*/
public function onAfterDispatch(): bool
{
// Do Nothing
return true;
}
/**
* Called by view=register to allow adapters to run it's queries before redirecting
*
* @since 1.0.0
*/
public function beforeStartRedirection(): void
{
// Nothing to do
}
/**
* If new user user is registered then set user id in session for the registration
*
* @param $user
* @param $isnew
*
* @return bool
*
* @since 1.0.0
*
* @noinspection PhpUnusedParameterInspection
*/
public function onBeforeStoreUser($user, $isnew): bool
{
// Do Nothing
return true;
}
/**
* Whenever a new user account is created on the site, the respective plugins would trigger this
*
* @param $user
* @param $isnew
* @param $success
* @param $msg
*
* @return bool
*
* @since 1.0.0
*
* @noinspection PhpUnusedParameterInspection
*/
public function onAfterStoreUser($user, $isnew, $success, $msg): bool
{
$this->setNewUserId($user['id']);
return true;
}
/**
* Triggered by registration plugin
*
* @since 1.0.0
*/
public function onPayplansAccessCheck(): void
{
// Do Nothing
}
/**
* This is where the magic happens! onAfterRoute is responsible for listening for
* events where the user wants to do registration actions and routing them appropriately,
* especially ensuring hitting the main Joomla registration endpoint is not done manually
* without considering plans.
*
* This is being called by the system plugin to perform checks on:
*
* - Registration without plan
* - Should Start Registration and complete registration
*
* @return bool
*
* @since 1.0.0
*
* @noinspection PhpUndefinedMethodInspection (invoice is called magically)
*/
public function onAfterRoute(): bool
{
// We should check if this plugin is enabled before redirecting user to plan page
if (!JPluginHelper::isEnabled('payplans', 'registration')) {
return false;
}
$option = (string)$this->input->get('option', '', 'default');
$task = (string)$this->input->get('task', '', 'default');
if (($option === 'com_users' || $option === 'com_payplans') && ($task !== 'trigger')) {
// We are interested in hits to com_users or com_payplans. We wish to ignore task=trigger
// though, as we don't care when cron calls in.
$view = (string)$this->input->get('view', '', 'default');
$layout = (string)$this->input->get('layout', '', 'default');
if ($option === 'com_users' && $view === 'registration' && $layout === 'complete') {
// This is the registration complete page, meaning the user has finished registering. At
// this point, we have the new user's ID because onAfterStoreUser was called and we put it
// in our session. But we'll make sure, of course.
//
// NOTE! The user may not be logged-in yet, if activation hasn't happened. So we need to get
// the userId from the session.
$userId = $this->getNewUserId();
$invoiceId = $this->getInvoiceId(true);
if (!$userId || !$invoiceId) {
// The odd case where this page is hit but we didn't actually register anyone. Likely a bot
// doing page scraping.
return false;
}
// Now link up the PayPlans information with the user that was created.
$invoice = PP::invoice($invoiceId);
$order = $invoice->getOrder();
$subscription = $order->getSubscription();
$invoice->user_id = $userId;
$invoice->save();
$order->buyer_id = $userId;
$order->save();
$subscription->user_id = $userId;
$subscription->save();
$this->updateNewUserPublishState($invoice, $userId);
// We're all done. Remove the Plan ID and Invoice ID from the session
$this->resetSessionVars();
// Now the user will either be sent to checkout or thanks. Checkout if they selected
// a plan that costs money, or thanks if they selected a free plan.
//
// If the user abandons at this point on a paid plan, they'll have a valid account
// (presuming they activated) but an unpaid plan. This is cool, as the registration
// portion of the flow is complete as far as this adapter is concerned.
$this->redirectToCheckout($invoiceId);
} else {
// We've been triggered, and we're interested in this route.
$isRegistrationUrl = $this->isRegistrationUrl();
$planId = $this->getPlanId();
/*
JFactory::getMailer()->sendMail(
'info@thechipwitch.com', 'Tester', 'tester@concellation.com', 'Test Spew',
"OAR Says Hook Time\n\n" . print_r($this->my, true) .
"\n\n" . print_r($this->input, true) . "\n\n" .
"iru = " . print_r($isRegistrationUrl, true) . ", Plan ID = $planId");*/
// Now check and see if this is a call to the base registration URL without
// a plan selected. If so, send the user to pick a plan.
if ($isRegistrationUrl && $planId) {
// The user is going directly to the registration URL, and there is a plan ID. But is
// there really? Because if they're hitting the registration URL, the plan ID and invoice
// key should also (or only) be on the query string! Let's find out!
$pTest = (int)$this->input->get('plan_id', 0, 'int');
$kTest = $this->input->get('invoice_key', '', 'default');
if (!$pTest || !$kTest) {
// Well! They're trying something funny! Let's clear anything in the session and send
// them to the plan selection page.
$this->session->set('REGISTRATION_NEW_USER_ID', 0);
$this->resetSessionVars();
$redirect = PPR::_('index.php?option=com_payplans&view=plan');
PP::redirect($redirect);
}
}
if ($isRegistrationUrl && !$planId) {
// Since there was no planId in the query string or in the session, this is a brand new
// shopping trip. Thus, make sure we don't have a new user ID laying around from a previous
// usage and clear out any session vars. We're starting from scratch.
$this->session->set('REGISTRATION_NEW_USER_ID', 0);
$this->resetSessionVars();
$redirect = PPR::_('index.php?option=com_payplans&view=plan');
PP::redirect($redirect);
}
}
}
return true;
}
}