I'm creating a Typescript React Native app (with Expo) and am trying to scaffold an "invite a user feature" where you input a nickname and email, then that user will receive an email inviting them to join the app.
It works by creating a Firestore collection for invitations and mails
And then pushes them to Trigger Email from Firestore, which then pushes to Mailgun via SMTP.
The invitation and mail are being successfully created in Firestore, but I'm getting a timeout error on Google console and nothing is appearing in Mailgun.
Here are my console logs:
INFO 2024-04-23T18:51:39.332357Z [resource.labels.functionName: ext-firestore-send-email-processQueue] Initializing extension with configuration
DEBUG 2024-04-23T18:51:39.402786533Z [resource.labels.functionName: ext-firestore-send-email-processQueue] [labels.executionId: y8j24zp92do1] Function execution started
INFO 2024-04-23T18:51:39.645436Z [resource.labels.functionName: ext-firestore-send-email-processQueue] [labels.executionId: y8j24zp92do1] Started execution of extension with configuration
INFO 2024-04-23T18:51:43.879766Z [resource.labels.functionName: ext-firestore-send-email-processQueue] [labels.executionId: y8j24zp92do1] Completed execution of extension
DEBUG 2024-04-23T18:51:43.883830637Z [resource.labels.functionName: ext-firestore-send-email-processQueue] [labels.executionId: y8j24zp92do1] Function execution took 4481 ms, finished with status: 'ok'
DEBUG 2024-04-23T18:52:35.972814575Z [resource.labels.functionName: ext-firestore-send-email-processQueue] [labels.executionId: yfsqgcwx6njq] Function execution took 59999 ms, finished with status: 'timeout'
INFO 2024-04-23T18:52:46.694433Z [resource.labels.functionName: ext-firestore-send-email-processQueue] Initializing extension with configuration
To expand on the timeout error:
{
insertId: "15ci506ffa9z6x"
labels: {2}
logName: "projects/icecreamsync/logs/cloudfunctions.googleapis.com%2Fcloud-functions"
receiveTimestamp: "2024-04-23T18:52:35.983830631Z"
resource: {2}
severity: "DEBUG"
textPayload: "Function execution took 59999 ms, finished with status: 'timeout'"
timestamp: "2024-04-23T18:52:35.972814575Z"
trace: "projects/icecreamsync/traces/651c7307ad3a2528ac08440baec5accb"
}
Here is the responsible code:
// index.ts
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
import { NodeMailgun } from "ts-mailgun";
admin.initializeApp();
// Mailgun instance
const mailer = new NodeMailgun();
mailer.apiKey = "api";
mailer.domain = "sandboxcxxxxx.mailgun.org";
mailer.fromEmail = "noreply@xxxxx.mailgun.org";
mailer.fromTitle = "App Name";
mailer.init();
// Cloud Function to send an email
export const sendEmailOnCreate = functions.runWith({
timeoutSeconds: 120, // Set higher timeout
memory: '256MB' // Set adequate memory
}).firestore.document("mail/{mailId}").onCreate(async (snap, context) => {
const mailData = snap.data();
if (!mailData.to || !mailData.subject || !mailData.html) {
console.error("Required email fields are missing", mailData);
return null; // terminate function if essential fields are missing
}
try {
await mailer.send(mailData.to, mailData.subject, mailData.html);
console.log("Mail sent successfully");
return null;
} catch (error) {
console.error("Failed to send email:", error);
return null; // consider logging this error to a persistent store
}
});
//FirebaseService.ts
import { db } from '../firebaseConfig';
import { collection, addDoc, serverTimestamp } from 'firebase/firestore';
export async function inviteFriend(friendNickname: string, friendEmail: string, userId: string): Promise<string> {
const invitation = {
nickname: friendNickname,
email: friendEmail,
invitationSent: true, // Set to true assuming email will be sent
status: 'pending',
invitedBy: userId,
createdAt: serverTimestamp()
};
try {
const docRef = await addDoc(collection(db, "invitations"), invitation);
console.log("Invitation created with ID: ", docRef.id);
// Create a document in the mail collection expected by the Trigger Email extension
const emailContent = {
to: friendEmail,
message: {
subject: "You've been invited!",
html: `<p>Hello ${friendNickname},</p><p>You have been invited by ${userId}. Click here to accept the invitation.</p>`
}
};
await addDoc(collection(db, "mail"), emailContent);
return docRef.id;
} catch (error) {
console.error("Error creating invitation and sending email:", error);
throw error;
}
}
// SettingsScreen.tsx
import React, { useState } from 'react';
import { View, TextInput, Button, Text, Alert, StyleSheet } from 'react-native';
import { inviteFriend } from '../services/firebaseService';
const SettingsScreen: React.FC = () => {
const [email, setEmail] = useState<string>('');
const [nickname, setNickname] = useState<string>('');
const handleInvite = async () => {
const userId = "user123"; // This should ideally be fetched from your auth state
try {
// Pass UserID here
const invitationId = await inviteFriend(nickname, email, userId);
Alert.alert("Success", `Invitation sent successfully! ID: ${invitationId}`);
} catch (error) {
console.error("Failed to send invitation:", error);
Alert.alert("Error", "Failed to send invitation.");
}
};
return (
<View style={styles.container}>
<Text>Invite a Friend</Text>
<TextInput
style={styles.input}
placeholder="Nickname"
value={nickname}
onChangeText={setNickname}
/>
<TextInput
style={styles.input}
placeholder="Email"
value={email}
onChangeText={setEmail}
/>
<Button title="Invite" onPress={handleInvite} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20
},
input: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
marginBottom: 10,
padding: 10
}
});
export default SettingsScreen;
The issue was that I was combining SMTP and API. I also needed to reinit my firestore function. I also removed the extension entirely.
Here's the code that eventually worked:
import * as functions from 'firebase-functions';
import { NodeMailgun } from 'ts-mailgun';
// Mailgun Configuration
const mailer = new NodeMailgun();
mailer.apiKey = '';
mailer.domain = '.org';
mailer.fromEmail = '';
mailer.fromTitle = '';
mailer.init();
// Firestore Trigger for Invitation
exports.sendInvitationEmail = functions.firestore
.document('invitations/{invitationId}')
.onCreate(async (snap, context) => {
const data = snap.data();
const email = data.email;
const nickname = data.nickname;
try {
await mailer.send(email, 'You are invited!', `<h1>Hello ${nickname}, you have been invited!</h1>`);
console.log('Invitation sent to', email);
} catch (error) {
console.error('Mail sending error:', error);
throw new functions.https.HttpsError('unknown', 'Failed to send invitation');
}
});
and
import React, { useState } from 'react';
import { View, TextInput, Button, StyleSheet, Text } from 'react-native';
import { db } from '../firebaseConfig'; //
import { collection, addDoc } from 'firebase/firestore';
export default function SettingsScreen() {
const [email, setEmail] = useState<string>('');
const [nickname, setNickname] = useState<string>('');
async function handleInvite() {
try {
await addDoc(collection(db, 'invitations'), {
email: email,
nickname: nickname,
createdAt: new Date(),
invitationSent: false,
status: 'pending'
});
alert('Invitation sent!');
} catch (error) {
console.error(error);
alert('Failed to send invitation.');
}
}
return (
<View style={styles.container}>
<Text>Email:</Text>
<TextInput style={styles.input} value={email} onChangeText={setEmail} placeholder="Enter email" />
<Text>Nickname:</Text>
<TextInput style={styles.input} value={nickname} onChangeText={setNickname} placeholder="Enter nickname" />
<Button title="Send Invitation" onPress={handleInvite} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
padding: 20,
},
input: {
height: 40,
marginBottom: 12,
borderWidth: 1,
padding: 10,
},
});