Customer Account and Contact Hooks

1 views Markdown

The fourteen hooks over the customer account and site contact: API keys, the profile, currency, the contact form and the newsletter.

Overview

The traces a customer or visitor leaves on the site live here: API keys, the profile, the currency preference, the contact form and the newsletter.

In most of these hooks the data comes from an unverified source: the visitor filling in the contact form, the address signing up, the bot requesting a page that does not exist. Check it yourself before carrying it to an outside service.

Reference

Following API key permissions changing

actionclient.api_key_updated
AccountApiKeys the new permissions only

Runs after the permissions of an API key change. A widening of permissions is a security event.

Parameters 3
$owner_idintThe owner of the key.
$idintThe id of the key record.
$permsarrayThe new permission set. ? The old set is not passed: to answer "did permissions widen" you must have kept the previous state yourself.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:client.api_key_updated', 10, function ($owner_id, $id, $perms) {
    // The old set is not passed: keep it yourself to compare.
    Acme::recordScopes($id, $perms);
});

Following an API key being regenerated

actionclient.api_key_regenerated
AccountApiKeys the record stays

Runs after the value of an API key is regenerated. The record stays, the credential changes: calls made with the old value no longer pass.

Parameters 2
$owner_idintThe owner of the key.
$idintThe record id; it is the same after the regeneration.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:client.api_key_regenerated', 10, function ($owner_id, $id) {
    // The old value no longer passes: refresh your own cache.
    Acme::invalidateCachedKey($id);
});

Following an API key being deleted

actionclient.api_key_deleted
AccountApiKeys the record is gone

Runs after an API key is deleted.

Parameters 2
$owner_idintThe owner of the key.
$idintThe id of the deleted record; the row is gone.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:client.api_key_deleted', 10, function ($owner_id, $id) {
    Acme::revokeIntegration($id);
});

Following a customer profile being updated

actionclient.profile_updated
AccountProfile the account, not the login

Runs after a customer profile is updated.

Parameters 3
$uidintThe id of the account whose profile changed. ? It is not the id of whoever signed in: a sub-user with the right permission can update another account’s profile. If you record the actor, read it from the session separately.
$data_updatesarrayThe fields written to the main record, new values only: name, phone, language, currency.
$info_updatesarrayWhat was written to the extra record: customer kind, company details, address fields.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:client.profile_updated', 10, function ($uid, $data_updates, $info_updates) {
    // $uid is the ACCOUNT id, not the login.
    Acme::syncCrm($uid, $data_updates + $info_updates);
});

Following a currency change

actionclient.currency_changed
website empty for a guest

Runs when a visitor changes the site currency. The change is already applied.

Parameters 3
$new_cidintThe new currency.
$old_cidintThe previous currency.
$memberarrayThe session of the signed-in member. It is empty for a guest: this hook runs for visitors who are not signed in too.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:client.currency_changed', 10, function ($new_cid, $old_cid, $member) {
    // For a guest the member data arrives EMPTY.
    if (!$member) return;

    Acme::rememberCurrency((int) ($member['id'] ?? 0), $new_cid);
});

Following a page that was not found

actionclient.page_not_found
website bot traffic included

Runs when a requested address does not resolve. It is the most direct way to find broken links.

Parameters 1
$urlstringThe full address requested. ? This hook also runs on addresses tried by scanning bots, and on a site those can outnumber human traffic. A listener writing a record on every call fills its own table with noise.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:client.page_not_found', 10, function ($url) {
    // Bot traffic can outnumber human traffic: filter it.
    if (Acme::looksLikeScanner($url)) return;

    Acme::noteBrokenLink($url);
});

Following a contact form submission

actionclient.contact_submitted
ClientContact comes from a visitor

Runs after a visitor submits the contact form.

Parameters 6
$message_idintThe id of the message record created.
$full_namestringThe sender’s name.
$emailstringThe sender’s address.
$phonestringTheir phone; it can be empty.
$messagestringThe message body, stripped of markup.
$ipstringTheir network address. All of these come from a visitor and none are verified: check them yourself before sending them to an outside service.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:client.contact_submitted', 10,
    function ($message_id, $full_name, $email, $phone, $message, $ip) {
        // All of these come FROM A VISITOR and are unverified.
        Acme::pushToCrm($message_id, $email, $message);
    });

Following a contact message being answered

actionclient.contact_message_replied
ClientContact after the reply

Runs after an administrator answers a contact message.

Parameters 2
$messagearrayThe original message answered.
$admin_messagestringThe reply text sent.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:client.contact_message_replied', 10, function ($message, $admin_message) {
    Acme::closeCrmCase((int) ($message['id'] ?? 0));
});

Following a message being reported as spam

actionclient.contact_message_spam_reported
ClientContact two separate flags

Runs after a contact message is reported as spam. The two flags beside it say how far the action went.

Parameters 3
$messagearrayThe message moved to spam, with its address and phone.
$block_emailsintWhether the address and phone went onto the block list.
$report_spamintWhether the network address was blocked. The two flags are independent: one can be set and the other not. A listener treating them as one reports the wrong thing.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:client.contact_message_spam_reported', 10,
    function ($message, $block_emails, $report_spam) {
        // The two flags are independent.
        if ($report_spam) Acme::shareBadAddress($message['ip'] ?? '');
    });

Stopping a message becoming a ticket

gateclient.contact_message_to_ticket
ClientContact before the conversion

Runs before a contact message is turned into a support ticket.

Parameters 3
$messagearrayThe message to be converted.
$departmentintThe target department.
$staffintThe staff member to assign; a zero means none.
Return 1
string|nullA non-empty text blocks the operation and is shown to the visitor as the error. An empty return lets it carry on.
Listener
Hook::add('gate:client.contact_message_to_ticket', 10,
    function ($message, $department, $staff) {
        if (Acme::blockedSender($message['email'] ?? '')) return 'No ticket can be opened for this sender.';

        return null;
    });

Changing the contact page data

filterclient.contact_page_data
ClientContact added keys reach the template

Runs before the contact page is shown. The offices and support hours are already resolved for the active language.

Parameters 2
$page_dataarrayby linkThe page data. Every key you add becomes a template variable: you can use it directly in your theme.
$langstringby linkThe active language.
Return 1
voidThe return is ignored; you write over the data.
Listener
Hook::add('filter:client.contact_page_data', 10, function (&$page_data, &$lang) {
    // A key you add becomes a template variable.
    $page_data['acme_map'] = Acme::mapEmbed($lang);
});

Stopping a newsletter sign-up

gateclient.newsletter_subscribe
ClientNewsletter before the record

Runs before an address joins the newsletter list. The place to keep disposable addresses out.

Parameters 2
$emailstringThe candidate address, lower-cased with its format checked.
$langstringThe site language at the moment of signing up.
Return 1
string|nullA non-empty text blocks the operation and is shown to the visitor as the error. An empty return lets it carry on.
Listener
Hook::add('gate:client.newsletter_subscribe', 10, function ($email, $lang) {
    if (Acme::disposableDomain($email)) return 'This address is not accepted.';

    return null;
});

Following a newsletter sign-up

actionclient.newsletter_subscribed
ClientNewsletter the language is kept too

Runs after an address joins the newsletter list.

Parameters 3
$emailstringThe subscriber address.
$langstringThe language at sign-up. It is kept and decides the language of bulk mailings: get it wrong and the subscriber receives an email they cannot read.
$addedintThe id of the record created.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:client.newsletter_subscribed', 10, function ($email, $lang, $added) {
    // The language decides the content language of bulk mailings.
    Acme::syncMailingList($email, $lang);
});

Following a newsletter unsubscribe

actionclient.newsletter_unsubscribed
ClientNewsletter through a link

Runs after an address leaves the list. It happens through the link in the email, with no sign-in needed.

Parameters 1
$emailstringThe address removed from the list.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:client.newsletter_unsubscribed', 10, function ($email) {
    // Remove it from your own list too, or mailings carry on.
    Acme::dropFromMailingList($email);
});

Pitfalls

The id in the profile hook is the account id

The id handed to the update hook belongs to the account being changed, not to whoever made the change. A sub-user with the right permission can update another account’s profile. If you record "who did it", read it from the session separately.

The not-found hook fills with bot traffic

Scanning bots try addresses that do not exist all day, and on a site those requests easily outnumber human ones. A listener writing a record on every call inflates its own table and buries the real broken links. Put a filter in front of it.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.