Skip to content

Commit 1bb5667

Browse files
authored
feat: only paid accounts can use pylon chat (#7381)
1 parent 413c9b8 commit 1bb5667

6 files changed

Lines changed: 27 additions & 113 deletions

File tree

docs/docs/deployment-self-hosting/core-configuration/environment-variables.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ relevant section below for more details.
7777
the browser will use the frontend node server to send API requests. Do not prepend `api/v1/` - it will be added
7878
automatically.
7979
- `GOOGLE_ANALYTICS_API_KEY`: Google Analytics key to track API usage.
80-
- `CRISP_WEBSITE_ID`: Crisp Chat widget Website key.
80+
- `PYLON_APP_ID`: Pylon in-app chat widget App ID.
8181
- `FIRST_PROMOTER_ID`: First Promoter ID for checkout affiliates.
8282
- `ALLOW_SIGNUPS`: **DEPRECATED** in favour of `PREVENT_SIGNUP` in the API. Determines whether to prevent manual signups
8383
without invites. Set it to any value to allow signups.

frontend/.eslintrc.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ module.exports = {
1414
],
1515
'globals': {
1616
'$': true,
17-
'$crisp': true,
1817
'API': true,
1918
'AccountProvider': true,
2019
'AccountStore': true,

frontend/api/dev-routes.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ module.exports = function setupRoutes(app) {
3333
{ name: 'headway', value: process.env.HEADWAY_API_KEY },
3434
{ name: 'ga', value: process.env.GOOGLE_ANALYTICS_API_KEY },
3535
{ name: 'sha', value: sha },
36-
{ name: 'crispChat', value: process.env.CRISP_WEBSITE_ID },
3736
{ name: 'pylonAppId', value: process.env.PYLON_APP_ID },
3837
{ name: 'fpr', value: process.env.FIRST_PROMOTER_ID },
3938
{ name: 'sentry', value: process.env.SENTRY_API_KEY },

frontend/api/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ app.get('/config/project-overrides', (req, res) => {
6666
{ name: 'headway', value: process.env.HEADWAY_API_KEY },
6767
{ name: 'ga', value: process.env.GOOGLE_ANALYTICS_API_KEY },
6868
{ name: 'sha', value: sha },
69-
{ name: 'crispChat', value: process.env.CRISP_WEBSITE_ID },
7069
{ name: 'pylonAppId', value: process.env.PYLON_APP_ID },
7170
{ name: 'fpr', value: process.env.FIRST_PROMOTER_ID },
7271
{ name: 'sentry', value: process.env.SENTRY_API_KEY },

frontend/common/loadChat.ts

Lines changed: 26 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,33 @@
1+
declare global {
2+
interface Window {
3+
Pylon?: ((...args: unknown[]) => void) & { q?: unknown[][] }
4+
pylon?: { chat_settings: Record<string, string | undefined> }
5+
}
6+
}
7+
18
import flagsmith from '@flagsmith/flagsmith'
2-
import moment from 'moment'
39
import AccountStore from './stores/account-store'
4-
import { getStore } from './store'
5-
import { selectBuildVersion } from './services/useBuildVersion'
610
import getUserDisplayName from './utils/getUserDisplayName'
7-
import { AccountModel, Organisation } from './types/responses'
11+
import Utils, { planNames } from './utils/utils'
12+
import { AccountModel } from './types/responses'
813
import Project from './project'
914

10-
// Used in self-hosted pages where users can opt into reaching out
11-
const defaultCrispID = '8857f89e-0eb5-4263-ab49-a293872b6c19'
1215
const defaultPylonID = '028babb7-d93f-4e32-be6a-59db190a084f'
1316

14-
async function loadCrisp(crispWebsiteId: string) {
15-
// @ts-ignore
16-
if (window.$crisp) {
17-
return
18-
}
19-
// @ts-ignore
20-
window.$crisp = []
21-
// @ts-ignore
22-
window.CRISP_WEBSITE_ID = crispWebsiteId
23-
await new Promise((resolve, reject) => {
24-
const d = document
25-
const s = d.createElement('script')
26-
s.src = 'https://client.crisp.chat/l.js'
27-
s.async = true
28-
s.onload = resolve
29-
s.onerror = reject
30-
d.getElementsByTagName('head')[0].appendChild(s)
31-
})
17+
function isFreePlan(): boolean {
18+
const plan = AccountStore.getActiveOrgPlan()
19+
return Utils.getPlanName(plan) === planNames.free
3220
}
3321

3422
async function loadPylon(pylonAppId: string) {
35-
// @ts-ignore
3623
if (window.Pylon) {
3724
return
3825
}
3926

40-
// Initialize Pylon using their recommended script format
4127
await new Promise((resolve, reject) => {
4228
const t = document
43-
const n = function (...args: any[]) {
44-
// @ts-ignore
45-
n.q.push(args)
46-
}
47-
// @ts-ignore
48-
n.q = []
49-
// @ts-ignore
29+
const n: ((...args: unknown[]) => void) & { q?: unknown[][] } =
30+
Object.assign((...args: unknown[]) => n.q!.push(args), { q: [] })
5031
window.Pylon = n
5132

5233
const s = t.createElement('script')
@@ -60,57 +41,8 @@ async function loadPylon(pylonAppId: string) {
6041
})
6142
}
6243

63-
function setupCrisp() {
64-
const user = AccountStore.model as AccountModel
65-
if (typeof $crisp === 'undefined' || !user) {
66-
return
67-
}
68-
const isSaas = () =>
69-
selectBuildVersion(getStore().getState())?.backend?.is_saas
70-
$crisp.push([
71-
'set',
72-
'session:data',
73-
[[['hosting', isSaas() ? 'SaaS' : 'Self-Hosted']]],
74-
])
75-
const organisation = AccountStore.getOrganisation() as Organisation
76-
const formatOrganisation = (o: Organisation) => {
77-
const plan = AccountStore.getActiveOrgPlan()
78-
return `${o.name} (${plan}) #${o.id}`
79-
}
80-
const otherOrgs = user?.organisations.filter((v) => v.id !== organisation?.id)
81-
if (window.$crisp) {
82-
$crisp.push(['set', 'user:email', user.email])
83-
$crisp.push(['set', 'user:nickname', `${getUserDisplayName(user)}`])
84-
if (otherOrgs.length) {
85-
$crisp.push([
86-
'set',
87-
'session:data',
88-
[[['other-orgs', `${otherOrgs?.length} other organisations`]]],
89-
])
90-
}
91-
$crisp.push([
92-
'set',
93-
'session:data',
94-
[
95-
[
96-
['user-id', `${user.id}`],
97-
['date-joined', `${moment(user.date_joined).format('Do MMM YYYY')}`],
98-
],
99-
],
100-
])
101-
if (organisation) {
102-
$crisp.push(['set', 'user:company', formatOrganisation(organisation)])
103-
$crisp.push([
104-
'set',
105-
'session:data',
106-
[[['seats', organisation.num_seats]]],
107-
])
108-
}
109-
}
110-
}
111-
11244
function setupPylon() {
113-
const user = AccountStore.model as AccountModel
45+
const user = AccountStore.getUser() as AccountModel
11446
if (typeof window.Pylon === 'undefined' || !user) {
11547
return
11648
}
@@ -131,24 +63,19 @@ function setupPylon() {
13163
}
13264

13365
export function identifyChatUser() {
134-
const usePylon = flagsmith.hasFeature('pylon_chat')
135-
136-
if (usePylon) {
66+
if (flagsmith.hasFeature('pylon_chat') && !isFreePlan()) {
13767
setupPylon()
138-
} else {
139-
setupCrisp()
14068
}
14169
}
14270

14371
export function openChat() {
144-
const usePylon = flagsmith.hasFeature('pylon_chat')
145-
146-
if (usePylon && typeof window.Pylon !== 'undefined') {
72+
if (
73+
flagsmith.hasFeature('pylon_chat') &&
74+
!isFreePlan() &&
75+
typeof window.Pylon !== 'undefined'
76+
) {
14777
window.Pylon('show')
14878
setupPylon()
149-
} else if (typeof $crisp !== 'undefined') {
150-
$crisp.push(['do', 'chat:open'])
151-
setupCrisp()
15279
}
15380
}
15481

@@ -157,14 +84,11 @@ export default async function loadChat(forceDefaultAPIKey?: boolean) {
15784
const isWidget = document.location.href.includes('/widget')
15885
if (isWidget) return
15986

160-
const usePylon = flagsmith.hasFeature('pylon_chat')
161-
const pylonId = forceDefaultAPIKey ? defaultPylonID : Project.pylonAppId
162-
const crispId = forceDefaultAPIKey ? defaultCrispID : Project.crispChat
163-
164-
if (usePylon && pylonId) {
165-
await loadPylon(pylonId)
166-
} else if (crispId) {
167-
await loadCrisp(crispId)
87+
if (flagsmith.hasFeature('pylon_chat') && !isFreePlan()) {
88+
const pylonId = forceDefaultAPIKey ? defaultPylonID : Project.pylonAppId
89+
if (pylonId) {
90+
await loadPylon(pylonId)
91+
}
16892
}
16993
} catch (error) {
17094
console.error('Failed to initialize chat widget:', error)

frontend/global.d.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@ export type OpenConfirm = {
1010
noText?: string
1111
}
1212
import { TooltipProps } from './web/components/Tooltip'
13-
type CrispCommand = [command: string, ...args: any[]]
14-
type Crisp = {
15-
// The push method accepts a CrispCommand array.
16-
push: (command: CrispCommand) => void
17-
}
1813
declare namespace UniversalAnalytics {
1914
interface PageviewFieldsObject {
2015
hitType: 'pageview' | 'event'
@@ -30,7 +25,6 @@ declare global {
3025
command: 'send',
3126
fields: UniversalAnalytics.PageviewFieldsObject,
3227
): void
33-
const $crisp: Crisp
3428
const delighted: {
3529
survey: (opts: {
3630
createdAt: string
@@ -98,7 +92,6 @@ declare global {
9892

9993
interface Window {
10094
E2E: boolean
101-
$crisp: Crisp
10295
engagement: {
10396
init(apiKey: string, options?: InitOptions): void
10497
plugin(): unknown

0 commit comments

Comments
 (0)