The repo for Purrform's main BigCommerce store.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: integrate Klaviyo API for email subscription in Diet Builder

+249 -3
+1
.eslintignore
··· 1 1 /assets/js/bundle.js 2 2 /assets/dist 3 + config.json 3 4 stencil.conf.js 4 5 webpack.conf.js
+231 -2
assets/js/theme/custom/diet-builder.js
··· 1110 1110 this.renderStep('Almost done!', content); 1111 1111 } 1112 1112 1113 - submitEmailForm(form) { 1113 + async submitEmailForm(form) { 1114 1114 const email = form.querySelector('input[name="email"]').value; 1115 1115 1116 1116 this.state.payload = { ··· 1127 1127 })), 1128 1128 }; 1129 1129 1130 - this.renderSuccessStep(); 1130 + this.renderLoadingStep(); 1131 + 1132 + const success = await this.runKlaviyoSequence(this.state.payload); 1133 + 1134 + if (success) { 1135 + this.renderSuccessStep(); 1136 + } else { 1137 + this.renderErrorStep(); 1138 + } 1139 + } 1140 + 1141 + async runKlaviyoSequence(payload) { 1142 + const publicKey = this.context.klaviyoPublicKey; 1143 + 1144 + if (!publicKey) { 1145 + // eslint-disable-next-line no-console 1146 + console.warn('[DietBuilder] Klaviyo public key not configured — skipping Klaviyo sequence.'); 1147 + return false; 1148 + } 1149 + 1150 + const profileId = await this.createKlaviyoProfile(payload.email, publicKey); 1151 + if (!profileId) return false; 1152 + 1153 + const subscribed = await this.subscribeToMarketing(profileId, payload.email, publicKey); 1154 + if (!subscribed) return false; 1155 + 1156 + return this.sendKlaviyoEvent(payload, publicKey); 1157 + } 1158 + 1159 + async createKlaviyoProfile(email, publicKey) { 1160 + const body = { 1161 + data: { 1162 + type: 'profile', 1163 + attributes: { email }, 1164 + }, 1165 + }; 1166 + 1167 + try { 1168 + const response = await fetch( 1169 + `https://a.klaviyo.com/client/profiles/?company_id=${encodeURIComponent(publicKey)}`, 1170 + { 1171 + method: 'POST', 1172 + headers: { 1173 + accept: 'application/json', 1174 + revision: '2025-01-15', 1175 + 'content-type': 'application/json', 1176 + }, 1177 + body: JSON.stringify(body), 1178 + }, 1179 + ); 1180 + 1181 + if (!response.ok) { 1182 + const errors = await response.json(); 1183 + const errorMsg = (errors.errors || []) 1184 + .map((e) => `[${e.status} - ${e.title} - ${e.detail}]`) 1185 + .join(', '); 1186 + // eslint-disable-next-line no-console 1187 + console.error(`[DietBuilder] Error creating Klaviyo profile: ${errorMsg}`); 1188 + return null; 1189 + } 1190 + 1191 + const data = await response.json(); 1192 + // eslint-disable-next-line no-console 1193 + console.log('[DietBuilder] Klaviyo profile created/found.'); 1194 + return data.data.id; 1195 + } catch (err) { 1196 + // eslint-disable-next-line no-console 1197 + console.error('[DietBuilder] Klaviyo createProfile error:', err); 1198 + return null; 1199 + } 1200 + } 1201 + 1202 + async subscribeToMarketing(profileId, email, publicKey) { 1203 + const body = { 1204 + data: { 1205 + type: 'subscription', 1206 + attributes: { 1207 + profile: { 1208 + data: { 1209 + type: 'profile', 1210 + id: profileId, 1211 + attributes: { 1212 + email, 1213 + subscriptions: { 1214 + email: { 1215 + marketing: { 1216 + consent: 'SUBSCRIBED', 1217 + }, 1218 + }, 1219 + }, 1220 + }, 1221 + }, 1222 + }, 1223 + }, 1224 + }, 1225 + }; 1226 + 1227 + try { 1228 + const response = await fetch( 1229 + `https://a.klaviyo.com/client/subscriptions/?company_id=${encodeURIComponent(publicKey)}`, 1230 + { 1231 + method: 'POST', 1232 + headers: { 1233 + accept: 'application/json', 1234 + revision: '2025-01-15', 1235 + 'content-type': 'application/json', 1236 + }, 1237 + body: JSON.stringify(body), 1238 + }, 1239 + ); 1240 + 1241 + if (!response.ok) { 1242 + const errors = await response.json(); 1243 + const errorMsg = (errors.errors || []) 1244 + .map((e) => `[${e.status} - ${e.title} - ${e.detail}]`) 1245 + .join(', '); 1246 + // eslint-disable-next-line no-console 1247 + console.error(`[DietBuilder] Error subscribing profile to marketing: ${errorMsg}`); 1248 + return false; 1249 + } 1250 + 1251 + // eslint-disable-next-line no-console 1252 + console.log('[DietBuilder] Profile subscribed to marketing.'); 1253 + return true; 1254 + } catch (err) { 1255 + // eslint-disable-next-line no-console 1256 + console.error('[DietBuilder] Klaviyo subscribeToMarketing error:', err); 1257 + return false; 1258 + } 1259 + } 1260 + 1261 + async sendKlaviyoEvent(payload, publicKey) { 1262 + const body = { 1263 + data: { 1264 + type: 'event', 1265 + attributes: { 1266 + metric: { 1267 + data: { 1268 + type: 'metric', 1269 + attributes: { 1270 + name: 'Diet Builder Completed', 1271 + }, 1272 + }, 1273 + }, 1274 + profile: { 1275 + data: { 1276 + type: 'profile', 1277 + attributes: { 1278 + email: payload.email, 1279 + }, 1280 + }, 1281 + }, 1282 + properties: { 1283 + catName: payload.catName, 1284 + calculatedRDA: payload.calculatedRDA, 1285 + recommendedProducts: payload.recommendedProducts, 1286 + }, 1287 + }, 1288 + }, 1289 + }; 1290 + 1291 + try { 1292 + const response = await fetch( 1293 + `https://a.klaviyo.com/client/events/?company_id=${encodeURIComponent(publicKey)}`, 1294 + { 1295 + method: 'POST', 1296 + headers: { 1297 + 'content-type': 'application/json', 1298 + revision: '2025-01-15', 1299 + }, 1300 + body: JSON.stringify(body), 1301 + }, 1302 + ); 1303 + 1304 + if (!response.ok) { 1305 + // eslint-disable-next-line no-console 1306 + console.error('[DietBuilder] Klaviyo event failed:', response.status, await response.text()); 1307 + return false; 1308 + } 1309 + 1310 + // eslint-disable-next-line no-console 1311 + console.log('[DietBuilder] Klaviyo event sent successfully.'); 1312 + return true; 1313 + } catch (err) { 1314 + // eslint-disable-next-line no-console 1315 + console.error('[DietBuilder] Klaviyo event error:', err); 1316 + return false; 1317 + } 1318 + } 1319 + 1320 + renderLoadingStep() { 1321 + const content = el( 1322 + 'div', 1323 + { className: 'diet-builder-loading' }, 1324 + el('p', { className: 'diet-builder-loading__message' }, 'Sending your results\u2026'), 1325 + ); 1326 + 1327 + this.renderStep('Almost there!', content); 1328 + } 1329 + 1330 + renderErrorStep() { 1331 + const retry = async () => { 1332 + this.renderLoadingStep(); 1333 + const success = await this.runKlaviyoSequence(this.state.payload); 1334 + if (success) { 1335 + this.renderSuccessStep(); 1336 + } else { 1337 + this.renderErrorStep(); 1338 + } 1339 + }; 1340 + 1341 + const content = el( 1342 + 'div', 1343 + { className: 'diet-builder-error' }, 1344 + el( 1345 + 'p', 1346 + { className: 'diet-builder-error__message' }, 1347 + 'Sorry, we had a problem sending your results. Please try again.', 1348 + ), 1349 + el( 1350 + 'button', 1351 + { 1352 + className: 'diet-builder-btn--primary', 1353 + onClick: retry, 1354 + }, 1355 + 'Try again', 1356 + ), 1357 + ); 1358 + 1359 + this.renderStep('Something went wrong', content); 1131 1360 } 1132 1361 1133 1362 renderSuccessStep() {
+1 -1
config.json
··· 1 1 { 2 - "name": "diet-builder - default font", 2 + "name": "diet-builder - backend", 3 3 "version": "6.10.0", 4 4 "template_engine": "handlebars_v4", 5 5 "meta": {
+15
schema.json
··· 3430 3430 ] 3431 3431 } 3432 3432 ] 3433 + }, 3434 + { 3435 + "name": "Integrations", 3436 + "settings": [ 3437 + { 3438 + "type": "heading", 3439 + "content": "Klaviyo" 3440 + }, 3441 + { 3442 + "type": "text", 3443 + "label": "Klaviyo Public API Key", 3444 + "id": "klaviyo_public_key", 3445 + "default": "" 3446 + } 3447 + ] 3433 3448 } 3434 3449 ]
+1
templates/layout/base.html
··· 42 42 {{~inject 'cartId' cart_id}} 43 43 {{~inject 'template' template}} 44 44 {{~inject 'storefrontAPIToken' settings.storefront_api.token}} 45 + {{~inject 'klaviyoPublicKey' theme_settings.klaviyo_public_key}} 45 46 {{~inject 'validationDictionaryJSON' (langJson 'validation_messages')}} 46 47 {{~inject 'validationFallbackDictionaryJSON' (langJson 'validation_fallback_messages')}} 47 48 {{~inject 'validationDefaultDictionaryJSON' (langJson 'validation_default_messages')}}