Search by

reactll / connect

yergunes

Reactll Connect for Laravel: visits, clicks, form leads, sales and site health for your Reactll dashboard.

v1.3.0 2026-10-14 18:39 UTC

This package is auto-updated.

Last update: 2026-10-14 18:39:55 UTC


README

Connects a Laravel site to its Reactll dashboard: visits, calls, WhatsApp and directions clicks, form leads, sales and site health. Built and maintained by Reactor Technology.

Install

composer require reactll/connect
REACTLL_CONNECT_TOKEN=rcc_…   # Reactll → Sites → the site → Install
php artisan reactll:heartbeat   # checks the connection

That's it: the script is added to every HTML page of the web group (not to your admin paths, not for signed-in users). Reactll asks the app for its health every hour (GET /api/reactll/health, signed — nothing else gets an answer), so no scheduler is needed; if one runs, reactll:heartbeat also reports hourly.

Form leads

After your form is validated and saved:

use Reactll\Connect\Facades\Connect;

Connect::submission('Contact', $request->only('name', 'email', 'phone', 'message'));

The script stamps every form with the page view id (_rc_pv), so Reactll ties the lead to the visit it came from. It never throws; it returns false if Reactll could not be reached. To send it after the visitor already has their response (recommended), use Connect::submitAfterResponse(...) with the same arguments. Connect::submit(...) returns the whole answer (a Reactll\Connect\Result, see below) instead of a bool.

Modules

Four switches, all on by default. A module switched off sends nothing (and Reactll drops anything that still arrives). They are reported to Reactll with the hourly health answer and reactll:heartbeat, together with capabilities (forms: the app sends its leads — REACTLL_CONNECT_SENDS_LEADS; sales: the sales module is on).

Module Off means
visitors no script on the pages
events the script counts page views only (data-events="off": no call / WhatsApp / click events)
forms submission() / submitAfterResponse() send nothing (submit() returns reason: switched_off)
sales order(), orders(), deleteOrders(), sendOrders() send nothing (reason: switched_off)

Sales & orders

Custom shops and SaaS send their sales as orders (POST /v1/orders, contract: Reactll's CONNECT-ORDERS). The app decides what an order is:

What happened Send
A one-off purchase / checkout one order, created_via: checkout, items = what was bought
A subscription payment (first or renewal, e.g. a paid Stripe invoice) one order per paid invoice, id = the invoice id, created_via: subscription, one item = the plan
A refund the same order again with refunded set (status: refunded when fully refunded) — a newer updated_at replaces it
A failed payment the order with status: failed (shows in the failed-payment alert, never as revenue)
A trial / free sign-up not an order — send it as a submission (form Signup)

Status: completed (paid), pending, failed, refunded, cancelled. Money as numbers or strings (sent as decimal strings in the order's currency, never converted), dates as Carbon / DateTime / unix time / ISO strings (sent as ISO 8601 with offset). created_at is required, updated_at defaults to now.

Personal data stays in the app. Pass the buyer's email: it becomes customer.key (HMAC-SHA256 of the lowercased email with the site's secret — Reactll cannot reverse it) and is never sent. No email (Apple / TikTok sign-in)? Pass customer_id (or user_id): hashed the same way with an id: prefix, never sent either; the email wins when both are given. Only First L., city and country go with it; phone, address, postcode and any key not in the contract are dropped.

use Reactll\Connect\Facades\Connect;

// A one-off checkout
Connect::order([
    'id' => $order->id,
    'status' => 'completed',
    'currency' => 'EUR',
    'total' => $order->total,          // 86.4 or "86.40"
    'tax' => $order->tax,
    'created_at' => $order->created_at,
    'paid_at' => $order->paid_at,
    'email' => $order->email,          // hashed, never sent
    'customer' => ['name' => $order->name, 'city' => $order->city, 'country' => 'DE'],
    'items' => [['name' => 'Pro plan', 'quantity' => 1, 'total' => $order->total, 'sku' => 'PRO']],
    'source' => ['type' => 'utm', 'source' => $user->utm_source, 'medium' => $user->utm_medium, 'campaign' => $user->utm_campaign],
    'admin_url' => route('admin.orders.show', $order),
]);

// A Stripe webhook (Cashier or your own): invoice.paid, invoice.payment_failed, credit_note.created / charge.refunded
// (send the invoice again). No stripe/stripe-php needed: an array or an object.
$invoice = $payload['data']['object'];
if ($order = Connect::fromStripeInvoice($invoice, ['customer' => ['returning' => null]])) {
    Connect::order($order);   // paid → completed, refunded → refunded, a failed attempt → failed
}

// A refund made in the app
Connect::order([...$sameOrder, 'refunded' => 86.40, 'status' => 'refunded', 'updated_at' => now()]);

// A failed payment
Connect::order([...$sameOrder, 'status' => 'failed', 'updated_at' => now()]);

// An order deleted in the app (a test order)
Connect::deleteOrders([$order->id]);

// A free sign-up is a lead, not an order
Connect::submitAfterResponse('Signup', ['plan' => 'trial', 'email' => $user->email]);

Never slows the request. order(), orders() and deleteOrders() normalise right away and send once the response is out (app()->terminating, one request per 50 orders, each order once per request). From a command or a queue worker they send at once; with REACTLL_CONNECT_QUEUE=true (or a connection name) they go through a queued Reactll\Connect\Jobs\SendOrders job instead. What Reactll could not take (down, unreachable) waits in the cache (up to 500 orders, 7 days) and is sent again after Reactll's hourly health call and by reactll:heartbeat. Connect::sendOrders($orders, $deleted = [], $backfill = false) sends now and returns Reactll's answer.

Every call returns a Reactll\Connect\Result and never throws:

Field
ok it arrived (or nothing had to be sent)
stored, updated, deleted, skipped Reactll's counts (skipped also counts orders without id, created_at, currency or total)
salesAllowed Reactll has the client's agreement for sales (false → nothing is stored yet)
reason queued, switched_off, not_allowed, host (the app URL is not the site's in Reactll — a staging copy), unreachable, refused, not_connected
queued orders and deletions waiting to be sent

site_url is config('app.url'): it has to match the site's address in Reactll.

Subscriptions (MRR, churn, trials)

A subscription business also sends the subscriptions themselves (POST /v1/subscriptions): Reactll counts MRR, new and cancelled subscribers and trials from them, while the paid invoices above stay the revenue. Send one when it starts, renews, changes plan, fails to renew or is cancelled — or resend them all from a scheduled command: a repeat with an older or equal updated_at changes nothing.

Connect::subscription([
    'id' => $sub->id,
    'plan' => 'Pro · Monthly',
    'status' => 'active',              // trialing, active, past_due, paused, canceled (cancelled / expired / unpaid accepted)
    'currency' => 'USD',
    'amount' => 9.99,                  // per interval, after discounts, excl. tax
    'interval' => 'month',             // day, week, month, year (monthly / yearly accepted)
    'started_at' => $sub->created_at,
    'current_period_end' => $sub->renews_at,
    'canceled_at' => $sub->canceled_at,
    'ends_at' => $sub->ends_at,        // cancelled at period end: access and MRR run to here
    'updated_at' => $sub->updated_at,
    'email' => $user->email,           // or customer_id — hashed, never sent
    'customer' => ['name' => $user->name],
]);

Connect::sendSubscriptions($all, $deletedIds);              // now, batches of 50, returns a Result
Connect::fromStripeSubscription($stripeSub, ['plan' => 'Pro', 'email' => $email]);   // customer.subscription.* webhooks

// Or on the invoice order itself: `subscription` links the payment to it (currency and customer come from the order)
Connect::order([...$invoiceOrder, 'subscription' => ['id' => $sub->id, 'status' => 'active', 'amount' => 9.99, 'interval' => 'month', 'updated_at' => now()]]);

Unlike orders, subscriptions have no retry queue: send them from a job or a scheduled command, which runs again.

Environment

Variable Default
REACTLL_CONNECT_TOKEN — the connection token (Reactll → Sites → the site → Install)
REACTLL_CONNECT_INJECT true add the script to every HTML page of the web group
REACTLL_CONNECT_COUNT_LOGGED_IN false count signed-in users
REACTLL_CONNECT_VISITORS true visitors module
REACTLL_CONNECT_EVENTS true events module
REACTLL_CONNECT_FORMS true forms module
REACTLL_CONNECT_SALES true sales module
REACTLL_CONNECT_SENDS_LEADS true the app sends its form leads (reported as capabilities.forms)
REACTLL_CONNECT_QUEUE false send orders from a queued job (true or a connection name) — only when a worker runs

Options

php artisan vendor:publish --tag=reactll-connect — paths to skip, counting signed-in users, the modules, turning the automatic script off (then put @reactllConnect in your layout's <head>).

Content-Security-Policy

If the site sends a CSP header, allow Reactll Connect in it, or the browser blocks the script and its beacons:

script-src  … https://connect.reactll.com
connect-src … https://connect.reactll.com

License

MIT © Reactor Technology