Skip to content

Commit 39ee275

Browse files
committed
fix: resolve merge conflict in u/[token].vue
2 parents d25f865 + 417a9dc commit 39ee275

File tree

15 files changed

+1982
-1189
lines changed

15 files changed

+1982
-1189
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ backend/config/default.yml
2020
supabase/config.toml
2121
frontend-v4/*
2222
.worktrees/
23+
.opencode/plans/

frontend/src/components/campaigns/CampaignComposerDialog.vue

Lines changed: 100 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -334,19 +334,35 @@
334334
:label="t('send_campaign')"
335335
:loading="isSubmitting"
336336
:disabled="isActionDisabled"
337-
@click="submit"
337+
@click="submit(false)"
338338
/>
339339
</div>
340340
</div>
341341
</template>
342342
</Dialog>
343+
344+
<ComplianceDialog ref="complianceDialogRef" @confirm-partial="submit(true)" />
345+
346+
<component
347+
:is="CreditsDialog"
348+
ref="CreditsDialogCampaignRef"
349+
engagement-type="contact"
350+
action-type="campaign"
351+
@secondary-action="submit(true)"
352+
/>
343353
</template>
344354

345355
<script setup lang="ts">
346356
import type { Contact } from '@/types/contact';
347357
import { extractUnavailableSenderEmails } from '@/utils/senderOptions';
348358
import { updateMiningSourcesValidityFromUnavailable } from '@/utils/sources';
349359
import Editor from 'primevue/editor';
360+
import ComplianceDialog from './ComplianceDialog.vue';
361+
import {
362+
CreditsDialog,
363+
CreditsDialogCampaignRef,
364+
openCreditsDialog,
365+
} from '@/utils/credits';
350366
351367
const isVisible = defineModel<boolean>('visible', { required: true });
352368
@@ -402,6 +418,9 @@ type SenderOptionItem = {
402418
const senderOptions = ref<SenderOptionItem[]>([]);
403419
const fallbackSenderEmail = ref('');
404420
const runtimeConfig = useRuntimeConfig();
421+
const complianceDialogRef = ref<InstanceType<typeof ComplianceDialog> | null>(
422+
null,
423+
);
405424
406425
const DEFAULT_PROJECT_URL = 'https://example.com/project';
407426
const DEFAULT_PROJECT_IMAGE_SRC =
@@ -623,7 +642,6 @@ const editorModules = computed(() => ({
623642
}));
624643
625644
type AttributeTarget = 'subject' | 'body';
626-
627645
const activeAttributeTarget = ref<AttributeTarget>('body');
628646
629647
const attributeMenuItems = computed(() =>
@@ -1046,7 +1064,7 @@ async function sendPreview() {
10461064
}
10471065
}
10481066
1049-
async function submit() {
1067+
async function submit(partialCampaign = false) {
10501068
if (!ensureValidForm()) {
10511069
return;
10521070
}
@@ -1056,6 +1074,9 @@ async function submit() {
10561074
}
10571075
10581076
isSubmitting.value = true;
1077+
let shouldCloseDialog = true;
1078+
let showErrorToast = true;
1079+
10591080
try {
10601081
const data = await $saasEdgeFunctions('email-campaigns/campaigns/create', {
10611082
method: 'POST',
@@ -1073,42 +1094,90 @@ async function submit() {
10731094
trackClick: form.trackClick,
10741095
plainTextOnly: form.plainTextOnly,
10751096
onlyValidContacts: form.onlyValidContacts,
1097+
partialCampaign,
1098+
},
1099+
onResponse: ({ response }) => {
1100+
if (response.status === 402) {
1101+
openCreditsDialog(
1102+
CreditsDialogCampaignRef,
1103+
true,
1104+
response._data.total,
1105+
response._data.available,
1106+
response._data.availableAlready,
1107+
);
1108+
shouldCloseDialog = false;
1109+
showErrorToast = false;
1110+
return;
1111+
}
1112+
1113+
if (response.status === 266 && response._data?.reason === 'credits') {
1114+
openCreditsDialog(
1115+
CreditsDialogCampaignRef,
1116+
false,
1117+
response._data.total,
1118+
response._data.available,
1119+
response._data.availableAlready,
1120+
);
1121+
shouldCloseDialog = false;
1122+
showErrorToast = false;
1123+
return;
1124+
}
1125+
1126+
if (response.status === 266 && response._data?.reason === 'consent') {
1127+
complianceDialogRef.value?.openModal(
1128+
response._data.total,
1129+
response._data.available,
1130+
);
1131+
shouldCloseDialog = false;
1132+
showErrorToast = false;
1133+
return;
1134+
}
1135+
1136+
if (response.status === 200) {
1137+
return;
1138+
}
1139+
1140+
throw new Error(response._data?.error || 'Campaign creation failed');
10761141
},
10771142
});
10781143
1079-
$toast.add({
1080-
group: 'has-links',
1081-
severity: 'success',
1082-
summary: t('campaign_started'),
1083-
detail: {
1084-
message: t('campaign_started_detail'),
1085-
button: {
1086-
text: t('see_campaigns'),
1087-
action: () => navigateTo('/campaigns'),
1144+
if (shouldCloseDialog) {
1145+
$toast.add({
1146+
group: 'has-links',
1147+
severity: 'success',
1148+
summary: t('campaign_started'),
1149+
detail: {
1150+
message: t('campaign_started_detail'),
1151+
button: {
1152+
text: t('see_campaigns'),
1153+
action: () => navigateTo('/campaigns'),
1154+
},
10881155
},
1089-
},
1090-
life: 6000,
1091-
});
1156+
life: 6000,
1157+
});
10921158
1093-
if (data?.campaignId) {
1094-
startCampaignCompletionWatcher(data.campaignId);
1095-
}
1159+
if (data?.campaignId) {
1160+
startCampaignCompletionWatcher(data.campaignId);
1161+
}
10961162
1097-
isVisible.value = false;
1098-
} catch (error: unknown) {
1099-
const parsedError = error as EdgeResponseError;
1100-
const code = parsedError?.data?.code;
1101-
if (code === 'SENDER_SMTP_FAILED' && fallbackSenderEmail.value) {
1102-
form.senderEmail =
1103-
parsedError?.data?.fallbackSenderEmail || fallbackSenderEmail.value;
1163+
isVisible.value = false;
11041164
}
1165+
} catch (error: unknown) {
1166+
if (showErrorToast) {
1167+
const parsedError = error as EdgeResponseError;
1168+
const code = parsedError?.data?.code;
1169+
if (code === 'SENDER_SMTP_FAILED' && fallbackSenderEmail.value) {
1170+
form.senderEmail =
1171+
parsedError?.data?.fallbackSenderEmail || fallbackSenderEmail.value;
1172+
}
11051173
1106-
$toast.add({
1107-
severity: 'error',
1108-
summary: t('campaign_start_failed'),
1109-
detail: resolveErrorMessage(error, 'error_campaign_start_failed'),
1110-
life: 5000,
1111-
});
1174+
$toast.add({
1175+
severity: 'error',
1176+
summary: t('campaign_start_failed'),
1177+
detail: resolveErrorMessage(error, 'error_campaign_start_failed'),
1178+
life: 5000,
1179+
});
1180+
}
11121181
} finally {
11131182
isSubmitting.value = false;
11141183
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<template>
2+
<Dialog v-model:visible="showModal" modal :header="t('consent_required')">
3+
<p class="m-0">
4+
{{ t('consent_message', { available, total: t('contact', total) }) }}
5+
</p>
6+
<template #footer>
7+
<div class="flex justify-end gap-2">
8+
<Button :label="t('privacy_policy')" link @click="openPrivacyPolicy" />
9+
<Button :label="t('cancel')" severity="secondary" @click="closeModal" />
10+
<Button
11+
:label="
12+
t('continue_with_available', { count: t('contact', available) })
13+
"
14+
severity="contrast"
15+
@click="confirmPartial"
16+
/>
17+
</div>
18+
</template>
19+
</Dialog>
20+
</template>
21+
22+
<script setup lang="ts">
23+
const { t } = useI18n({
24+
useScope: 'local',
25+
});
26+
27+
const emit = defineEmits<{
28+
'confirm-partial': [];
29+
}>();
30+
31+
const showModal = ref(false);
32+
const total = ref(0);
33+
const available = ref(0);
34+
35+
const openModal = (totalCount: number, availableCount: number) => {
36+
total.value = totalCount;
37+
available.value = availableCount;
38+
showModal.value = true;
39+
};
40+
41+
const closeModal = () => {
42+
showModal.value = false;
43+
};
44+
45+
const openPrivacyPolicy = () => {
46+
window.open('/privacy-policy', '_blank');
47+
};
48+
49+
const confirmPartial = () => {
50+
emit('confirm-partial');
51+
closeModal();
52+
};
53+
54+
defineExpose({
55+
openModal,
56+
closeModal,
57+
});
58+
</script>
59+
60+
<i18n lang="json">
61+
{
62+
"en": {
63+
"consent_required": "Consent Required",
64+
"consent_message": "Only {available} of {total} have given consent to be contacted.",
65+
"contact": "contact | contacts",
66+
"privacy_policy": "Privacy Policy",
67+
"cancel": "Cancel",
68+
"continue_with_available": "Continue with {count}"
69+
},
70+
"fr": {
71+
"consent_required": "Consentement Requis",
72+
"consent_message": "Seulement {available} sur {total} ont donné leur consentement.",
73+
"contact": "contact | contacts",
74+
"privacy_policy": "Politique de Confidentialité",
75+
"cancel": "Annuler",
76+
"continue_with_available": "Continuer avec {count}"
77+
}
78+
}
79+
</i18n>
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
1-
import type { H3Event } from "h3";
1+
import type { H3Event } from 'h3';
22

33
export function buildEmailCampaignEdgeUrl(
44
event: H3Event,
55
path: string,
66
): string {
77
const config = useRuntimeConfig(event);
88
const baseUrl =
9-
config.public?.SAAS_SUPABASE_PROJECT_URL || process.env.SAAS_SUPABASE_PROJECT_URL;
9+
config.public?.SAAS_SUPABASE_PROJECT_URL ||
10+
process.env.SAAS_SUPABASE_PROJECT_URL;
1011

1112
if (!baseUrl) {
1213
throw createError({
1314
statusCode: 500,
14-
statusMessage: "Missing SAAS_SUPABASE_PROJECT_URL",
15+
statusMessage: 'Missing SAAS_SUPABASE_PROJECT_URL',
1516
});
1617
}
1718

18-
const normalizedBase = String(baseUrl).replace(/\/$/, "");
19-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
19+
const normalizedBase = String(baseUrl).replace(/\/$/, '');
20+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
2021
return `${normalizedBase}/functions/v1/email-campaigns${normalizedPath}`;
2122
}

0 commit comments

Comments
 (0)