Merge pull request #1634 from specklesystems/dim/fe2/newsletter-registration
feat(server): opt-in audience subscribing on registration
This commit is contained in:
@@ -63,6 +63,10 @@ graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
const newsletterConsent = ref(false)
|
||||
|
||||
provide('newsletterconsent', newsletterConsent)
|
||||
|
||||
const { result } = useQuery(loginServerInfoQuery)
|
||||
const { appId, challenge, inviteToken } = useLoginOrRegisterUtils()
|
||||
|
||||
|
||||
@@ -43,6 +43,19 @@
|
||||
:class="`mt-2 overflow-hidden ${pwdFocused ? 'h-8' : 'h-0'} transition-[height]`"
|
||||
/>
|
||||
<FormButton submit full-width class="mt-4" :disabled="loading">Sign up</FormButton>
|
||||
<div
|
||||
class="mt-3 text-xs flex items-center justify-center text-foreground-2 space-x-2"
|
||||
>
|
||||
<!--
|
||||
Note the newsletter consent box is here because i got very confused re layout of the panel
|
||||
and didn't figure out a better way to put it where i needed it to be
|
||||
-->
|
||||
<FormCheckbox
|
||||
v-model="newsletterConsent"
|
||||
name="test"
|
||||
label="I want to receive tips and tricks on how to use Speckle"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="serverInfo.termsOfService"
|
||||
class="mt-2 text-xs text-foreground-2 text-center linkify-tos"
|
||||
@@ -99,6 +112,8 @@ const nameRules = [isRequired]
|
||||
const { signUpWithEmail, inviteToken } = useAuthManager()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
const newsletterConsent = inject<Ref<boolean>>('newsletterconsent')
|
||||
|
||||
const pwdFocused = ref(false)
|
||||
|
||||
const finalLoginRoute = computed(() => {
|
||||
@@ -116,7 +131,8 @@ const onSubmit = handleSubmit(async (fullUser) => {
|
||||
await signUpWithEmail({
|
||||
user,
|
||||
challenge: props.challenge,
|
||||
inviteToken: inviteToken.value
|
||||
inviteToken: inviteToken.value,
|
||||
newsletter: newsletterConsent?.value
|
||||
})
|
||||
} catch (e) {
|
||||
triggerNotification({
|
||||
|
||||
@@ -50,6 +50,8 @@ const {
|
||||
const mixpanel = useMixpanel()
|
||||
const { inviteToken } = useAuthManager()
|
||||
|
||||
const newsletterConsent = inject<Ref<boolean>>('newsletterconsent')
|
||||
|
||||
const NuxtLink = resolveComponent('NuxtLink')
|
||||
const GoogleButton = resolveComponent('AuthThirdPartyLoginButtonGoogle')
|
||||
const MicrosoftButton = resolveComponent('AuthThirdPartyLoginButtonMicrosoft')
|
||||
@@ -68,6 +70,11 @@ const buildAuthUrl = (strat: StrategyType) => {
|
||||
url.searchParams.set('token', inviteToken.value)
|
||||
}
|
||||
|
||||
if (newsletterConsent?.value) {
|
||||
url.searchParams.set('newsletter', 'true')
|
||||
}
|
||||
|
||||
console.log(url)
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
|
||||
@@ -239,14 +239,16 @@ export const useAuthManager = () => {
|
||||
}
|
||||
challenge: string
|
||||
inviteToken?: string
|
||||
newsletter?: boolean
|
||||
}) => {
|
||||
const { user, challenge, inviteToken } = params
|
||||
const { user, challenge, inviteToken, newsletter } = params
|
||||
|
||||
const { accessCode } = await registerAndGetAccessCode({
|
||||
apiOrigin,
|
||||
challenge,
|
||||
user,
|
||||
inviteToken
|
||||
inviteToken,
|
||||
newsletter
|
||||
})
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
|
||||
@@ -27,6 +27,7 @@ type RegisterParams = {
|
||||
apiOrigin: string
|
||||
challenge: string
|
||||
inviteToken?: string
|
||||
newsletter?: boolean
|
||||
user: {
|
||||
email: string
|
||||
password: string
|
||||
@@ -85,7 +86,7 @@ export async function getAccessCode(params: LoginParams) {
|
||||
}
|
||||
|
||||
export async function registerAndGetAccessCode(params: RegisterParams) {
|
||||
const { apiOrigin, challenge, user, inviteToken } = params
|
||||
const { apiOrigin, challenge, user, inviteToken, newsletter } = params
|
||||
if (!user.email || !user.password || !user.name) {
|
||||
throw new InvalidRegisterParametersError(
|
||||
"Can't register without a valid email, password and name!"
|
||||
@@ -98,6 +99,10 @@ export async function registerAndGetAccessCode(params: RegisterParams) {
|
||||
registerUrl.searchParams.append('token', inviteToken)
|
||||
}
|
||||
|
||||
if (newsletter) {
|
||||
registerUrl.searchParams.append('newsletter', 'true')
|
||||
}
|
||||
|
||||
const res = await fetch(registerUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable camelcase */
|
||||
import mailchimp from '@mailchimp/mailchimp_marketing'
|
||||
import { logger } from '@/logging/logging'
|
||||
import { md5 } from '@/modules/shared/helpers/cryptoHelper'
|
||||
import {
|
||||
getMailchimpConfig,
|
||||
getMailchimpStatus
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import { getUserById } from '@/modules/core/services/users'
|
||||
|
||||
async function addToMailchimpAudience(userId: string) {
|
||||
// Do not do anything (inc. logging) if we do not explicitely enable it
|
||||
if (!getMailchimpStatus()) return
|
||||
|
||||
// Note: fails here should not block registration at any cost
|
||||
try {
|
||||
const config = getMailchimpConfig() // Note: throws an error if not configured
|
||||
|
||||
mailchimp.setConfig({
|
||||
apiKey: config.apiKey,
|
||||
server: config.serverPrefix
|
||||
})
|
||||
|
||||
const user = await getUserById({ userId })
|
||||
|
||||
if (!user) {
|
||||
throw new Error(
|
||||
'Could not register user for newsletter - no db user record found.'
|
||||
)
|
||||
}
|
||||
|
||||
const [first, second] = user.name.split(' ')
|
||||
const subscriberHash = md5(user.email.toLowerCase())
|
||||
|
||||
// NOTE: using setListMember (NOT addListMember) to prevent errors for previously
|
||||
// registered members.
|
||||
await mailchimp.lists.setListMember(config.listId, subscriberHash, {
|
||||
status_if_new: 'subscribed',
|
||||
email_address: user.email,
|
||||
merge_fields: {
|
||||
EMAIL: user.email,
|
||||
FNAME: first,
|
||||
LNAME: second,
|
||||
FULLNAME: user.name // NOTE: this field needs to be set in the audience merge fields
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
logger.warn(e, 'Failed to register user to newsletter.')
|
||||
}
|
||||
}
|
||||
|
||||
export { addToMailchimpAudience }
|
||||
@@ -11,9 +11,10 @@ const { isSSLServer, getRedisUrl } = require('@/modules/shared/helpers/envHelper
|
||||
const { authLogger } = require('@/logging/logging')
|
||||
const { createRedisClient } = require('@/modules/shared/redis/redis')
|
||||
const { mixpanel, resolveMixpanelUserId } = require('@/modules/shared/utils/mixpanel')
|
||||
|
||||
const { addToMailchimpAudience } = require('./services/mailchimp')
|
||||
/**
|
||||
* TODO: Get rid of session entirely, we don't use it for the app and it's not really necessary for the auth flow, so it only complicates things
|
||||
* NOTE: it does seem used!
|
||||
*/
|
||||
|
||||
module.exports = async (app) => {
|
||||
@@ -49,6 +50,11 @@ module.exports = async (app) => {
|
||||
req.session.token = token
|
||||
}
|
||||
|
||||
const newsletterConsent = req.query.newsletter || null
|
||||
if (newsletterConsent) {
|
||||
req.session.newsletterConsent = true
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
@@ -64,6 +70,9 @@ module.exports = async (app) => {
|
||||
challenge: req.session.challenge
|
||||
})
|
||||
|
||||
let newsletterConsent = false
|
||||
if (req.session.newsletterConsent) newsletterConsent = true // NOTE: it's only set if it's true
|
||||
|
||||
if (req.session) req.session.destroy()
|
||||
|
||||
// Resolve redirect URL
|
||||
@@ -83,6 +92,10 @@ module.exports = async (app) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (newsletterConsent) {
|
||||
await addToMailchimpAudience(req.user.id)
|
||||
}
|
||||
|
||||
const redirectUrl = urlObj.toString()
|
||||
|
||||
return res.redirect(redirectUrl)
|
||||
|
||||
@@ -81,6 +81,26 @@ export function getOidcName() {
|
||||
return process.env.OIDC_NAME
|
||||
}
|
||||
|
||||
export function getMailchimpStatus() {
|
||||
return [true, 'true'].includes(process.env.MAILCHIMP_ENABLED || false)
|
||||
}
|
||||
|
||||
export function getMailchimpConfig() {
|
||||
if (
|
||||
!process.env.MAILCHIMP_API_KEY ||
|
||||
!process.env.MAILCHIMP_SERVER_PREFIX ||
|
||||
!process.env.MAILCHIMP_LIST_ID
|
||||
) {
|
||||
throw new MisconfiguredEnvironmentError('Mailchimp is not configured')
|
||||
}
|
||||
|
||||
return {
|
||||
apiKey: process.env.MAILCHIMP_API_KEY,
|
||||
serverPrefix: process.env.MAILCHIMP_SERVER_PREFIX,
|
||||
listId: process.env.MAILCHIMP_LIST_ID
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get app base url / canonical url / origin
|
||||
* TODO: Go over all getBaseUrl() usages and move them to getXOrigin() instead
|
||||
|
||||
@@ -36,9 +36,11 @@
|
||||
"@aws-sdk/lib-storage": "^3.100.0",
|
||||
"@godaddy/terminus": "^4.9.0",
|
||||
"@graphql-tools/schema": "^9.0.4",
|
||||
"@mailchimp/mailchimp_marketing": "^3.0.80",
|
||||
"@sentry/node": "^6.17.9",
|
||||
"@sentry/tracing": "^6.17.9",
|
||||
"@speckle/shared": "workspace:^",
|
||||
"@types/mailchimp__mailchimp_marketing": "^3.0.9",
|
||||
"@types/pino-http": "^5.8.1",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"apollo-server-express": "^3.10.2",
|
||||
|
||||
@@ -276,6 +276,27 @@ spec:
|
||||
- name: EMAIL_FROM
|
||||
value: "{{ .Values.server.email.from }}"
|
||||
{{- end }}
|
||||
|
||||
# *** Newsletter ***
|
||||
{{- if (default false (.Values.server.mailchimp).enabled) }}
|
||||
- name: MAILCHIMP_ENABLED
|
||||
value: {{ default false (.Values.server.mailchimp).enabled }}
|
||||
- name: MAILCHIMP_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ default .Values.secretName ((.Values.server.mailchimp).apikey).secretName }}
|
||||
key: {{ default "mailchimp_apikey" ((.Values.server.mailchimp).apikey).secretKey }}
|
||||
- name: MAILCHIMP_SERVER_PREFIX
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ default .Values.secretName ((.Values.server.mailchimp).serverprefix).secretName }}
|
||||
key: {{ default "mailchimp_serverprefix" ((.Values.server.mailchimp).serverprefix).secretKey }}
|
||||
- name: MAILCHIMP_LIST_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ default .Values.secretName ((.Values.server.mailchimp).listid).secretName }}
|
||||
key: {{ default "mailchimp_listid" ((.Values.server.mailchimp).listid).secretKey }}
|
||||
{{- end }}
|
||||
|
||||
# *** Tracking / Tracing ***
|
||||
- name: SENTRY_DSN
|
||||
|
||||
@@ -9344,6 +9344,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mailchimp/mailchimp_marketing@npm:^3.0.80":
|
||||
version: 3.0.80
|
||||
resolution: "@mailchimp/mailchimp_marketing@npm:3.0.80"
|
||||
dependencies:
|
||||
dotenv: ^8.2.0
|
||||
superagent: 3.8.1
|
||||
checksum: a43f69334766bf02fccf4f6076fa38b7e6a53d9d99485547e1e11d23f9188349ac2831a329f15bb8e31be46cca5e22275a120d0d108b80212ed92ad12df709e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mapbox/node-pre-gyp@npm:^1.0.0":
|
||||
version: 1.0.9
|
||||
resolution: "@mapbox/node-pre-gyp@npm:1.0.9"
|
||||
@@ -11237,6 +11247,7 @@ __metadata:
|
||||
"@graphql-codegen/typescript-operations": ^2.5.2
|
||||
"@graphql-codegen/typescript-resolvers": 2.7.2
|
||||
"@graphql-tools/schema": ^9.0.4
|
||||
"@mailchimp/mailchimp_marketing": ^3.0.80
|
||||
"@sentry/node": ^6.17.9
|
||||
"@sentry/tracing": ^6.17.9
|
||||
"@speckle/objectloader": "workspace:^"
|
||||
@@ -11251,6 +11262,7 @@ __metadata:
|
||||
"@types/ejs": ^3.1.1
|
||||
"@types/express": ^4.17.13
|
||||
"@types/lodash": ^4.14.180
|
||||
"@types/mailchimp__mailchimp_marketing": ^3.0.9
|
||||
"@types/mjml": ^4.7.0
|
||||
"@types/mocha": ^10.0.0
|
||||
"@types/mock-require": ^2.0.1
|
||||
@@ -15321,6 +15333,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mailchimp__mailchimp_marketing@npm:^3.0.9":
|
||||
version: 3.0.9
|
||||
resolution: "@types/mailchimp__mailchimp_marketing@npm:3.0.9"
|
||||
checksum: 3bf413367a77a331fd87552096ce42f23baee3f34309c91766d26095bc686fa541aa1ce5d72061fd66e9e2b8bb3589542b4709e04043c1ca54e28e71e4480934
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mdx@npm:^2.0.0":
|
||||
version: 2.0.3
|
||||
resolution: "@types/mdx@npm:2.0.3"
|
||||
@@ -25534,7 +25553,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"formidable@npm:^1.2.0":
|
||||
"formidable@npm:^1.1.1, formidable@npm:^1.2.0":
|
||||
version: 1.2.6
|
||||
resolution: "formidable@npm:1.2.6"
|
||||
checksum: 2b68ed07ba88302b9c63f8eda94f19a460cef6017bfda48348f09f41d2a36660c9353137991618e0e4c3db115b41e4b8f6fa63bc973b7a7c91dec66acdd02a56
|
||||
@@ -40206,6 +40225,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"superagent@npm:3.8.1":
|
||||
version: 3.8.1
|
||||
resolution: "superagent@npm:3.8.1"
|
||||
dependencies:
|
||||
component-emitter: ^1.2.0
|
||||
cookiejar: ^2.1.0
|
||||
debug: ^3.1.0
|
||||
extend: ^3.0.0
|
||||
form-data: ^2.3.1
|
||||
formidable: ^1.1.1
|
||||
methods: ^1.1.1
|
||||
mime: ^1.4.1
|
||||
qs: ^6.5.1
|
||||
readable-stream: ^2.0.5
|
||||
checksum: 42895e220fb5aab303edeef7ec4d9c38fef31638d18254fe57329366e7a74624dffe20acd682a9a27df4f62fe4acb953b33d16be9611f184284815c2492d35cf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"superagent@npm:^3.7.0, superagent@npm:^3.8.3":
|
||||
version: 3.8.3
|
||||
resolution: "superagent@npm:3.8.3"
|
||||
|
||||
Reference in New Issue
Block a user