Search code examples
reactjstypescriptfirebasereact-nativemailgun

Cannot successfully integrate Mailgun with "Trigger Email from Firestore" in my Typescript React Native app, getting timeout


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

enter image description here

enter image description here

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 = "[email protected]";
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;

Solution

  • 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,
        },
    });