diff --git a/apps/platform/src/providers/email/MailgunEmailProvider.ts b/apps/platform/src/providers/email/MailgunEmailProvider.ts index 4735856f..4b3bcf8e 100644 --- a/apps/platform/src/providers/email/MailgunEmailProvider.ts +++ b/apps/platform/src/providers/email/MailgunEmailProvider.ts @@ -38,7 +38,10 @@ export default class MailgunEmailProvider extends EmailProvider { type: 'object', required: ['api_key', 'domain'], properties: { - api_key: { type: 'string' }, + api_key: { + type: 'string', + title: 'API Key', + }, domain: { type: 'string' }, webhook_signing_key: { type: 'string', diff --git a/apps/platform/src/providers/email/SESEmailProvider.ts b/apps/platform/src/providers/email/SESEmailProvider.ts index d0651ac1..fc8a3c36 100644 --- a/apps/platform/src/providers/email/SESEmailProvider.ts +++ b/apps/platform/src/providers/email/SESEmailProvider.ts @@ -43,8 +43,14 @@ export default class SESEmailProvider extends EmailProvider { type: 'object', required: ['accessKeyId', 'secretAccessKey'], properties: { - accessKeyId: { type: 'string' }, - secretAccessKey: { type: 'string' }, + accessKeyId: { + type: 'string', + title: 'Access Key ID', + }, + secretAccessKey: { + type: 'string', + title: 'Secret Access Key', + }, }, }, }, diff --git a/apps/platform/src/providers/push/LocalPushProvider.ts b/apps/platform/src/providers/push/LocalPushProvider.ts index 74870315..e3832f73 100644 --- a/apps/platform/src/providers/push/LocalPushProvider.ts +++ b/apps/platform/src/providers/push/LocalPushProvider.ts @@ -12,6 +12,7 @@ interface APNParams { teamId: string } production: boolean + bundleId: string } interface FCMParams { @@ -46,27 +47,49 @@ export default class LocalPushProvider extends PushProvider { type: 'object', nullable: true, required: ['production', 'token'], + title: 'APN', + description: 'Settings for Apple Push Notifications to send messages to iOS devices.', properties: { production: { type: 'boolean', + description: 'Leave unchecked if you are wanting to send to sandbox devices only.', }, token: { type: 'object', required: ['key', 'keyId', 'teamId'], properties: { - key: { type: 'string' }, - keyId: { type: 'string' }, - teamId: { type: 'string' }, + key: { + type: 'string', + minLength: 80, + }, + keyId: { + type: 'string', + title: 'Key ID', + }, + teamId: { + type: 'string', + title: 'Team ID', + }, }, }, + bundleId: { + type: 'string', + title: 'Bundle ID', + }, }, }, fcm: { type: 'object', nullable: true, required: ['id'], + title: 'FCM', + description: 'Settings for Firebase Cloud Messaging to send messages to Android devices.', properties: { - id: { type: 'string' }, + id: { + type: 'string', + title: 'Server Key', + minLength: 80, + }, }, }, }, @@ -81,10 +104,11 @@ export default class LocalPushProvider extends PushProvider { } async send(push: Push): Promise { - const { tokens, title, topic, body, custom } = push + // TODO: Need a better way of bubbling up errors + const { tokens, title, body, custom } = push const response = await this.transport.send(typeof tokens === 'string' ? [tokens] : tokens, { title, - topic, + topic: this.apn?.bundleId, body, custom, }) diff --git a/apps/platform/src/providers/push/PushChannel.ts b/apps/platform/src/providers/push/PushChannel.ts index fba0a840..c7907265 100644 --- a/apps/platform/src/providers/push/PushChannel.ts +++ b/apps/platform/src/providers/push/PushChannel.ts @@ -7,6 +7,7 @@ export default class PushChannel { constructor(provider?: PushProvider) { if (provider) { this.provider = provider + this.provider.boot?.() } else { throw new Error('A valid push notification provider must be defined!') } @@ -17,6 +18,9 @@ export default class PushChannel { // Find tokens from active devices with push enabled const tokens = variables.user.pushEnabledDevices.map(device => device.token) + // If no tokens, don't send + if (tokens?.length <= 0) return + const push = { tokens, ...template.compile(variables), diff --git a/apps/platform/src/providers/text/NexmoTextProvider.ts b/apps/platform/src/providers/text/NexmoTextProvider.ts index 43ab179a..eaa99656 100644 --- a/apps/platform/src/providers/text/NexmoTextProvider.ts +++ b/apps/platform/src/providers/text/NexmoTextProvider.ts @@ -30,8 +30,14 @@ export default class NexmoTextProvider extends TextProvider { type: 'object', required: ['api_key', 'api_secret', 'phone_number'], properties: { - api_key: { type: 'string' }, - api_secret: { type: 'string' }, + api_key: { + type: 'string', + title: 'API Key', + }, + api_secret: { + type: 'string', + title: 'API Secret', + }, phone_number: { type: 'string' }, }, }) diff --git a/apps/platform/src/providers/text/PlivoTextProvider.ts b/apps/platform/src/providers/text/PlivoTextProvider.ts index 36288e5b..bc5d9a84 100644 --- a/apps/platform/src/providers/text/PlivoTextProvider.ts +++ b/apps/platform/src/providers/text/PlivoTextProvider.ts @@ -29,7 +29,10 @@ export default class PlivoTextProvider extends TextProvider { type: 'object', required: ['auth_id', 'auth_token', 'phone_number'], properties: { - auth_id: { type: 'string' }, + auth_id: { + type: 'string', + title: 'Auth ID', + }, auth_token: { type: 'string' }, phone_number: { type: 'string' }, }, diff --git a/apps/platform/src/providers/text/TwilioTextProvider.ts b/apps/platform/src/providers/text/TwilioTextProvider.ts index 26788db8..0d8ca095 100644 --- a/apps/platform/src/providers/text/TwilioTextProvider.ts +++ b/apps/platform/src/providers/text/TwilioTextProvider.ts @@ -37,7 +37,10 @@ export default class TwilioTextProvider extends TextProvider { type: 'object', required: ['account_sid', 'auth_token', 'phone_number'], properties: { - account_sid: { type: 'string' }, + account_sid: { + type: 'string', + title: 'Account SID', + }, auth_token: { type: 'string' }, phone_number: { type: 'string' }, }, diff --git a/apps/platform/src/render/Template.ts b/apps/platform/src/render/Template.ts index a045e79c..dcc9317f 100644 --- a/apps/platform/src/render/Template.ts +++ b/apps/platform/src/render/Template.ts @@ -167,7 +167,7 @@ export class PushTemplate extends Template { this.title = json?.data.title this.topic = json?.data.topic this.body = json?.data.body - this.custom = json?.data.custom + this.custom = json?.data.custom ?? {} } compile(variables: Variables): CompiledPush { diff --git a/apps/platform/src/render/TemplateService.ts b/apps/platform/src/render/TemplateService.ts index 68082239..34900b12 100644 --- a/apps/platform/src/render/TemplateService.ts +++ b/apps/platform/src/render/TemplateService.ts @@ -9,6 +9,8 @@ import { UserEvent } from '../users/UserEvent' import { loadTextChannel } from '../providers/text' import { RequestError } from '../core/errors' import CampaignError from '../campaigns/CampaignError' +import { loadPushChannel } from '../providers/push' +import { getUserFromEmail, getUserFromPhone } from '../users/UserRepository' export const pagedTemplates = async (params: SearchParams, projectId: number) => { return await Template.searchParams( @@ -65,20 +67,33 @@ export const sendProof = async (template: TemplateType, variables: Variables, re const campaign = await getCampaign(template.campaign_id, template.project_id) if (!campaign) throw new RequestError(CampaignError.CampaignDoesNotExist) + const event = UserEvent.fromJson(variables.event || {}) + const context = variables.context || {} + const projectId = template.project_id if (template.type === 'email') { - const channel = await loadEmailChannel(campaign.provider_id, template.project_id) + const channel = await loadEmailChannel(campaign.provider_id, projectId) await channel?.send(template, { user: User.fromJson({ ...variables.user, data: variables.user, email: recipient }), - event: UserEvent.fromJson(variables.event || {}), - context: variables.context || {}, + event, + context, }) } else if (template.type === 'text') { - const channel = await loadTextChannel(campaign.provider_id, template.project_id) + const channel = await loadTextChannel(campaign.provider_id, projectId) await channel?.send(template, { user: User.fromJson({ ...variables.user, data: variables.user, phone: recipient }), - event: UserEvent.fromJson(variables.event || {}), - context: variables.context || {}, + event, + context, + }) + } else if (template.type === 'push') { + const channel = await loadPushChannel(campaign.provider_id, projectId) + const user = await getUserFromEmail(projectId, recipient) ?? await getUserFromPhone(projectId, recipient) + if (!user) throw new RequestError('Unable to find a user matching the criteria.') + user.data = { ...variables.user, ...user.data } + await channel?.send(template, { + user, + event, + context, }) } else { throw new RequestError('Sending template proofs is only supported for email and text message types as this time.') diff --git a/apps/platform/src/subscriptions/SubscriptionController.ts b/apps/platform/src/subscriptions/SubscriptionController.ts index 809a6c32..104385ad 100644 --- a/apps/platform/src/subscriptions/SubscriptionController.ts +++ b/apps/platform/src/subscriptions/SubscriptionController.ts @@ -255,7 +255,7 @@ export const subscriptionCreateSchema: JSONSchemaType = { }, channel: { type: 'string', - enum: ['email', 'text', 'webhook'], + enum: ['email', 'text', 'push', 'webhook'], }, }, additionalProperties: false, diff --git a/apps/ui/src/ui/form/FormWrapper.tsx b/apps/ui/src/ui/form/FormWrapper.tsx index 4cdc9045..7c6d3859 100644 --- a/apps/ui/src/ui/form/FormWrapper.tsx +++ b/apps/ui/src/ui/form/FormWrapper.tsx @@ -26,7 +26,6 @@ export default function FormWrapper({ }) const handleSubmit = form.handleSubmit(async data => { - console.log('submitting', data) setIsLoading(true) onSubmit(data, navigate).finally(() => { setIsLoading(false) diff --git a/apps/ui/src/ui/form/SchemaFields.css b/apps/ui/src/ui/form/SchemaFields.css index 964ac42a..5473aa94 100644 --- a/apps/ui/src/ui/form/SchemaFields.css +++ b/apps/ui/src/ui/form/SchemaFields.css @@ -2,4 +2,16 @@ border: 1px solid var(--color-grey); border-radius: var(--border-radius); padding: 10px; +} + +.ui-schema-form > h5 { + margin-top: 15px; + margin-bottom: 2px; +} + +.ui-schema-form > p { + margin: 2px 0 10px; + color: var(--color-primary-soft); + font-weight: 400; + font-size: 14px; } \ No newline at end of file diff --git a/apps/ui/src/ui/form/SchemaFields.tsx b/apps/ui/src/ui/form/SchemaFields.tsx index fa7faa7c..064d910c 100644 --- a/apps/ui/src/ui/form/SchemaFields.tsx +++ b/apps/ui/src/ui/form/SchemaFields.tsx @@ -5,18 +5,22 @@ import SwitchField from './SwitchField' interface Schema { type: 'string' | 'number' | 'boolean' | 'object' + title?: string + description?: string properties?: Record required?: string[] + minLength?: number } interface SchemaProps { title?: string + description?: string parent: string schema: Schema form: any } -export default function SchemaFields({ title, parent, form, schema }: SchemaProps) { +export default function SchemaFields({ title, description, parent, form, schema }: SchemaProps) { if (!schema?.properties) { return <> } @@ -25,28 +29,40 @@ export default function SchemaFields({ title, parent, form, schema }: SchemaProp const keys = Object.keys(schema.properties) return
{title &&
{snakeToTitle(title)}
} + {description &&

{description}

}
{keys.map(key => { const item = props[key] const required = schema.required?.includes(key) + const title = item.title ?? snakeToTitle(key) if (item.type === 'string' || item.type === 'number') { return = 80} /> } else if (item.type === 'boolean') { return } else if (item.type === 'object') { - return SchemaFields({ title: key, form, parent: `${parent}.${key}`, schema: item }) + return } return 'no key' })} diff --git a/apps/ui/src/ui/form/SwitchField.tsx b/apps/ui/src/ui/form/SwitchField.tsx index f29d41f4..f1b1ebc8 100644 --- a/apps/ui/src/ui/form/SwitchField.tsx +++ b/apps/ui/src/ui/form/SwitchField.tsx @@ -32,7 +32,7 @@ export default function SwitchField onChange?.(event.target.checked)} - {...form?.register(name, { disabled, required })} + {...form?.register(name, { disabled })} />
diff --git a/apps/ui/src/views/campaign/CampaignPreview.tsx b/apps/ui/src/views/campaign/CampaignPreview.tsx index fe05b3a8..c471f71f 100644 --- a/apps/ui/src/views/campaign/CampaignPreview.tsx +++ b/apps/ui/src/views/campaign/CampaignPreview.tsx @@ -66,7 +66,7 @@ const SendProof = ({ open, onClose, onSubmit, type }: SendProofProps) => { open={open} onClose={onClose} title="Send Proof" - description={`Enter the ${type === 'email' ? 'email address' : 'phone number'} of the recipient you want to receive the proof of this template.`}> + description={`Enter the ${type === 'email' ? 'email address' : 'email or phone number'} of the recipient you want to receive the proof of this template.`}> onSubmit={async ({ recipient }) => await onSubmit(recipient)}> {form => ( diff --git a/apps/ui/src/views/campaign/TemplateDetail.tsx b/apps/ui/src/views/campaign/TemplateDetail.tsx index 0e51fcbf..6beeaa5c 100644 --- a/apps/ui/src/views/campaign/TemplateDetail.tsx +++ b/apps/ui/src/views/campaign/TemplateDetail.tsx @@ -64,7 +64,6 @@ const TextForm = ({ form }: { form: UseFormReturn }) const PushTable = ({ data }: { data: PushTemplateData }) => const PushForm = ({ form }: { form: UseFormReturn }) => <> @@ -79,11 +78,6 @@ const PushForm = ({ form }: { form: UseFormReturn }) label="Body" textarea required /> - interface TemplateDetailProps {