Building Fundraising Campaigns using Next.js, Donately, & Firebase

Leo Oliveira

Software Engineer

9 min

Article Banner

Introduction

This article is a comprehensive overview of the work done to bring to life a not-for-profit project for one of our clients; you can see the final product here (and if you are in Canada, consider joining this campaign for a great cause).

The guide below should be a good starting point for anyone trying to implement their donation campaigns or projects however, if it is all too overwhelming, consider reaching out to us and we can make your ideas come to life.

Table of Contents

  • Tech Stack
  • Why Donately?
  • Preparation
  • Understanding the Architecture


Tech Stack

The tech stack used to develop this project is not an absolute truth, a lot of these technologies have interchangeable variants so feel free to use the equivalent resources which you are most comfortable with (although we do highly recommend Donately as the donation platform of choice).

If you do proceed with the technologies mentioned, it is assumed that you have some familiarity with them. You can also check the documentation of the respective technologies to learn more about them at any point in time.

Frontend

We may or may not have a bias when it comes to our front-end development tools. Similarly to a lot of the work we do, for this specific project, we have opted to use Next.js and Vercel to handle our front-end development and deployment/hosting.

Backend

Due to the requirements of the project, we opted to use Firebase. Through it, we get access to a real-time database as well as email authentication.

Donation Platform

The donation platform which we have selected is Donately. The reason why we have selected this platform will be explained in the next section, so if you trust my good judgment, feel free to skip it. Suffice it to say, it is incredibly handy for all fundraising purposes.


Why Donately?

When working with non-profits, the number one concern is keeping costs to a minimum. Another particular concern, for us as developers, is having a good API to interact with. We researched various platforms (and all of them had very positive points to offer) yet Donately proved to be the most complete solution for the work we needed to do.

This project in particular is for cyclists to register and use their kilometers cycled as an incentive to collect donations from friends, family, and others who they share their profile link with; these donations are later directed towards the cause being campaigned for. That being said, we required the ability to create fundraising forms for each registered cyclist while being able to track the donations received toward their goal and Donately's API allowed us to do all of that.


Donately's free tier will also allow your registered users to easily start receiving donations almost immediately. There is a 4% platform fee under the free tier, which may not be the best for some, but they do offer the option in their donation forms for donors to cover the cost (and honestly, for the services they provide it's pretty generous). By default, Donately also creates a shareable donation page for your users; though upgrading to the Starter tier (which we used for this project) gives you embeddable forms, allowing you to keep the entire donation flow within your website.

Now that this platform choice has been explained, let's jump into the action.


Preparation

Before we get started, go ahead and create an account at Donately. Once you make it to your account's dashboard, there are a couple of steps you will need to follow:

  1. Click on Integrations which can be found under Settings on the side menu. Once on the Integrations page, you should be able to find your unique account ID as well as your API token; copy both as you will need them for configuring Donately inside your project.
  2. Click on Campaigns found under Fundraising on the side menu and click on the New button on the top right. It is at this point that implementations will differ (this guide only covers the most complicated option). If you plan on raising funds on your own select General Campaign. If you would like your site's users to collect funds for you, select Peer-to-Peer Fundraiser. Finish creating your campaign of choice and fetch your campaign ID.
  3. Click on Account Settings on the left sidebar. From there click on the Stripe Settings tab and connect a Stripe account so that you can receive all donations. Once it's connected, you should see a Stripe Publishable Key. Take note of this key as you will use it in your embeddable forms.

Head over to your project and create a .env file at the root level if you don't have one already and add the following properties.

1NEXT_PUBLIC_DONATELY_API_BASE_URL=https://api.donately.com/v2
2NEXT_PUBLIC_DONATELY_API_TOKEN=<YOUR_TOKEN_HERE>
3NEXT_PUBLIC_DONATELY_VERSION=2020-11-03
4NEXT_PUBLIC_DONATELY_ACCOUNT_ID=<YOUR_ACCOUNT_ID>
5NEXT_PUBLIC_DONATELY_CAMPAIGN_ID=<YOUR_CAMPAIGN_ID>
6NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=<YOUR_STRIPE_PUBLISHABLE_KEY>

As for the Firebase setup, if you haven't created a Firebase web project already, you can follow the official docs. You will need to include the following environment variables in your .env file, all of which can be found in your project settings.

1NEXT_PUBLIC_FIREBASE_API_KEY=
2NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
3NEXT_PUBLIC_FIREBASE_PROJECT_ID=
4NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=
5NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=
6NEXT_PUBLIC_FIREBASE_APP_ID=

With these properties in place, we are now ready to start development.


Understanding the Architecture

The reason why this guide does not cover a General Campaign is that a general campaign in its entirety does not need to be automated and can be entirely configured on Donately; you just need one donation form/donation page for all donors to utilize. On the other hand, with Peer-to-Peer, although the funds ultimately go to the one responsible for creating the campaign, each registered participant needs their own donation form/donation page, so making it work is more challenging.

For our purposes, we required the Peer-to-Peer Campaign flow, so most of our logic resides within our Sign Up/Profile Update functionality. For a quick overview:

Sign Up Page

  1. The user provides their details (email, first name, last name, fundraising goal, etc...)
  2. On form submission:
    1. Make a Donately API call to create a new Fundraiser for the user.
    2. Make a Firebase API call to register the user in the backend and another to update the user's database entry with their data.
    3. Handle any errors or redirect the user to their profile on success.

Profile Update

  1. The user provides their updated information
  2. On form submission:
    1. Make a Donately API call to update the user's Donately profile with new information if changed (first name, last name)
    2. Make another Donately API call to update the user's fundraiser with new data if changed (fundraising goal)
    3. Make a Firebase API call to update the user's database entry with new data
    4. Handle any errors or redirect the user to their profile on success

For your guidance, the code covered in the next section will address the steps displayed in the overview above through code comments. With that out of the way, let's get to coding!


The fun part!

As defined by our architecture, we will be making some Firebase & Donately API calls, so let's start by writing some helper functions to handle all the logic.

Our API requests will all be made through Axios, so start by adding the dependency to your project:

This project was finalized prior to Next 13. If you are using Next 13 and its app directory structure, it would be more ideal for you to use fetch instead of Axios as it offers plenty of features (i.e. caching) out-of-the-box which work well with NextJS
1npm install axios

Donately Helpers

Create a utility folder at your project root and add the following helper functions to a donately.ts file. As you add these helper functions, please go over the function comments to make sure you have a full understanding of what each function does. These should cover most (if not all) of the interactions that you will be having with Donately through your site.

1import axios, { AxiosRequestConfig } from "axios";
2
3/**
4 * Reusable configuration builder for all Donately API calls.
5 *
6 * @param method - HTTP request method
7 * @param path - API endpoint
8 * @param data - Data to be sent with the request
9 * @returns An Axios request configuration object
10 */
11const generateConfig = (
12  method: string,
13  path: string,
14  data?: object
15): AxiosRequestConfig<any> => ({
16  method,
17  url: `${process.env.NEXT_PUBLIC_DONATELY_API_BASE_URL}${path}`,
18  headers: {
19    Accept: "application/json",
20    Authorization: Buffer.from(
21      `${process.env.NEXT_PUBLIC_DONATELY_API_TOKEN}`,
22      "utf8"
23    ).toString("base64"),
24    "Donately-Version": `${process.env.NEXT_PUBLIC_DONATELY_VERSION}`,
25  },
26  data,
27});
28
29/**
30 * Creates a Donately Fundraiser with a specific **fundraiserTarget** for
31 * a specific user (**firstName**, **lastName**, **email**) from a specific campaign (identified by the **campaignId**).
32 *
33 * @link https://docs-api-v20190315.donately.com/#bef4ca48-1f5f-4069-af75-34f5ba63a988
34 *
35 * @param firstName - Registered user's first name.
36 * @param lastName - Registered user's last name.
37 * @param email - Registered user's email.
38 * @param description - Fundraiser description.
39 * @param fundraiserTarget - Fundraiser target.
40 */
41export const createDonatelyFundraiser = async (
42  firstName: string,
43  lastName: string,
44  email: string,
45  description: string,
46  fundraiserTarget: number
47) => {
48  const data = new FormData();
49  data.append("account_id", process.env.NEXT_PUBLIC_DONATELY_ACCOUNT_ID);
50  data.append(
51    "title",
52    `${firstName} ${lastName}${
53      lastName.charAt(lastName.length - 1).toLowerCase() === "s" ? "'" : "'s"
54    } Fundraiser`
55  );
56  data.append("description", description);
57  data.append("campaign_id", process.env.NEXT_PUBLIC_DONATELY_CAMPAIGN_ID);
58  data.append("goal_in_cents", String(fundraiserTarget * 100));
59  data.append("email", email);
60
61  try {
62    const config = generateConfig("post", "/fundraisers", data);
63    const response = await axios(config);
64    const fundraisersData = response.data;
65    // 1. Grab a person's ID and fundraiser ID from the response
66    const userId = fundraisersData.data.person.id;
67    const fundraiserId = fundraisersData.data.id;
68    // 2. Update the person on donately with their first and last name
69    await updateDonatelyUser(userId, firstName, lastName);
70    // 3. Create a form for the user attached to this specific campaign
71    const fundraiserFormId = await createDonatelyFundraiserForm(
72      fundraiserId,
73      firstName,
74      lastName
75    );
76
77    return {
78      donorId: userId,
79      fundraiserId: fundraiserId,
80      fundraiserFormId: fundraiserFormId,
81    };
82  } catch (e: any) {
83    throw new Error(e?.response?.data?.message);
84  }
85};
86
87/**
88 * Creates a Donately fundraiser form for a registered user's fundraiser, specified by the **fundraiserId**.
89 * A fundraiser form is required for displaying a donation form embed on the registered user's profile page.
90 *
91 * @link https://docs-api-v20190315.donately.com/#9f1d4f60-3ccd-4d2d-b3c9-c614ef1adda0
92 *
93 * @param fundraiserId - Registered user's Donately fundraiser id.
94 * @param firstName - Registered user's first name.
95 * @param lastName - Registered user's last name.
96 */
97export const createDonatelyFundraiserForm = async (
98  fundraiserId: string,
99  firstName: string,
100  lastName: string
101) => {
102  const data = new FormData();
103  data.append(
104    "title",
105    `${firstName}${
106      firstName.charAt(firstName.length - 1).toLowerCase() === "s" ? "'" : "'s"
107    } Fundraiser`
108  );
109  // Change these Donately configurations as needed. Check the POST request for /forms in Donately's API docs.
110  data.append(
111    "form_config",
112    JSON.stringify({
113      presets: "10, 50, 100",
114      amount: "25",
115      currency: "CAD",
116      payment_options: "cc, ach",
117      comment: true,
118      primary_color: "#DC6027",
119      dont_send_receipt_email: true,
120      campaign_id: process.env.NEXT_PUBLIC_DONATELY_CAMPAIGN_ID,
121      fundraiser_id: fundraiserId,
122      recurring_frequency: ["false"],
123    })
124  );
125
126  try {
127    const config = generateConfig("post", `/forms?account_id=${process.env.NEXT_PUBLIC_DONATELY_ACCOUNT_ID}`, data);
128    let response = await axios(config);
129    let formData = response.data;
130    return formData.data.id;
131  } catch (e: Error | any) {
132    console.log(e?.message);
133  }
134};
135
136/**
137 * Updates a registered user's Donately profile. Since all user registration is done
138 * only on the website, we must make sure to keep our registered user's information
139 * synchronized in Donately so that their fundraisers (which are generated through the code)
140 * are unique to them (first & last name).
141 *
142 * @link https://docs-api-v20190315.donately.com/#094868ea-a921-44ee-a1ec-c10525bd53d0
143 *
144 * @param userId - Donately userId associated to registered user.
145 * @param firstName - Registered user's first name.
146 * @param lastName - Registered user's last name.
147 */
148export const updateDonatelyUser = async (
149  userId: string,
150  firstName: string,
151  lastName: string
152) => {
153  const data = new FormData();
154  data.append("account_id", process.env.NEXT_PUBLIC_DONATELY_ACCOUNT_ID);
155  data.append("first_name", firstName);
156  data.append("last_name", lastName);
157
158  try {
159    const config = generateConfig("post", `/people/${userId}`, data);
160    await axios(config);
161  } catch (e: Error | any) {
162    console.log(e?.message);
163  }
164};
165
166/**
167 * Updates a registered user's fundraiser data. Since all user information is changed
168 * only on the website, we must make sure to keep our registered user's fundraiser
169 * synchronized in Donately so that the donation information we receive is accurate.
170 *
171 * @link https://docs-api-v20190315.donately.com/#07133889-a22e-4631-816a-e393b406db55
172 *
173 * @param fundraiserId - Donately fundraiser id associated to registered user.
174 * @param fundraisingGoal - Updated fundraising goal (in dollars).
175 */
176export const updateDonatelyFundraiser = async (
177  fundraiserId: string,
178  fundraisingGoal: number
179) => {
180  const data = new FormData();
181  data.append("account_id", process.env.NEXT_PUBLIC_DONATELY_ACCOUNT_ID);
182  data.append("goal_in_cents", `${fundraisingGoal * 100}`);
183
184  try {
185    const config = generateConfig("post", `/fundraisers/${fundraiserId}`, data);
186    await axios(config);
187  } catch (e: Error | any) {
188    console.log(e?.message);
189  }
190};
191
192/**
193 * Get a Donately fundraiser's information, identified by the provided **fundraiserId**.
194 *
195 * @link https://docs-api-v20190315.donately.com/#6c377971-9775-4c71-b906-cdf7acb22a56
196 *
197 * @param fundraiserId - Donately fundraiser id.
198 * @returns a fundraiser object.
199 */
200export const getDonatelyFundraiser = async (
201  fundraiserId: string | string[] | undefined
202) => {
203  if (fundraiserId == null) return;
204  try {
205    const config = generateConfig(
206      "get",
207      `/fundraisers/${fundraiserId}?account_id=${process.env.NEXT_PUBLIC_DONATELY_ACCOUNT_ID}`
208    );
209    const response = await axios(config);
210    return response.data;
211  } catch (e: Error | any) {
212    console.log(e?.message);
213  }
214};
215
216/**
217 * Retrieves the top 5 donations for a fundraiser specified by the **fundraiserId**.
218 *
219 * @link https://docs-api-v20190315.donately.com/#616c0fb2-97b8-4548-8ba9-951ae8a46d4c
220 *
221 * @param fundraiserId
222 * @returns
223 */
224export const getDonatelyDonationsForFundaiser = async (
225  fundraiserId: string | string[] | undefined
226) => {
227  if (fundraiserId == null) return;
228  const queryParams = `account_id=${process.env.NEXT_PUBLIC_DONATELY_ACCOUNT_ID}&campaign_id=${process.env.NEXT_PUBLIC_DONATELY_CAMPAIGN_ID}&fundraiser_id=${fundraiserId}&limit=5&order=desc&order_by=amount_in_cents`;
229  try {
230    const config = generateConfig("get", `/donations?${queryParams}`);
231    const response = await axios(config);
232    return response.data;
233  } catch (e: Error | any) {
234    console.log(e?.message);
235  }
236};

Firebase Helpers

In the same utility folder, add a firebase.ts file. These will help us with our signups and backend updates. This will require changes from your end depending on how you want your data set up in Firebase, but it should be a good starting point if you don't have a structure in mind.

1import { initializeApp } from "firebase/app";
2import {
3  collection,
4  doc,
5  DocumentData,
6  DocumentSnapshot,
7  getDoc,
8  getDocs,
9  getFirestore,
10  query,
11  QuerySnapshot,
12  setDoc,
13  where,
14  writeBatch,
15} from "firebase/firestore";
16import {
17  createUserWithEmailAndPassword,
18  getAuth,
19  sendPasswordResetEmail,
20  signInWithEmailAndPassword,
21  signOut,
22  UserCredential,
23} from "firebase/auth";
24
25const firebaseConfig = {
26  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
27  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
28  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
29  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
30  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
31  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
32};
33
34const firebaseApp = initializeApp(firebaseConfig);
35const firestore = getFirestore(firebaseApp);
36export const firebaseAuth = getAuth();
37
38/**
39 * Updates the database schema for a specified donor.
40 *
41 * @param rootCollection - The name of the Firebase collection in which your users are stored.
42 * @param data - fields to be updated in the database, following the structure of a DonorDataType object.
43 * @param authenticatedUser - A user object for the currently autheticated user in Firebase.
44 */
45export const updateDonor = async (
46  rootCollection: string,
47  data: DonorDataType,
48  authenticatedUser: UserType | null
49) => {
50  if (authenticatedUser?.uid != null) {
51    const donorsRef = doc(firestore, rootCollection, authenticatedUser.uid);
52    await setDoc(
53      donorsRef,
54      {
55        ...data,
56      },
57      { merge: true }
58    );
59  } else {
60    // If user somehow managed to trigger function without being
61    // authenticated, trigger Firebase Signout.
62    await signOut(firebaseAuth);
63  }
64};
65
66/**
67 * Retrieve firebase information for the donor identified by the provided userId.
68 *
69 * @param rootCollection - The name of the Firebase collection in which your users are stored.
70 * @param userId - Identifier for the user to retrieve information for.
71 * @returns A promise containing a firebase document snapshot. Call **.exists()** on returned snapshot to verify if document is available.
72 */
73export const getDonorInfo = async (
74  rootCollection: string,
75  userId: string
76): Promise<DocumentSnapshot<DocumentData>> => {
77  const donorsRef = doc(firestore, rootCollection, userId);
78  return getDoc(donorsRef);
79};
80
81/**
82 * Fetch all donor documents in Firebase.
83 * 
84 * @param rootCollection - The name of the Firebase collection in which your users are stored.
85 * @returns A promise that resolves to a QuerySnapshot containing all documents for each donor.
86 */
87export const getAllDonors = async (
88  rootCollection: string
89): Promise<QuerySnapshot<DocumentData>> => {
90  const q = query(collection(firestore, rootCollection));
91  return getDocs(q);
92};
93
94/**
95 * Signs up a user with the provided email and password using Firebase authentication.
96 *
97 * @param email - Registration email.
98 * @param password - Registration password.
99 * @returns a Promise that when resolved, returns a Firebase UserCredential object.
100 *
101 * [UserCredential object documentation] - {@link https://firebase.google.com/docs/reference/js/auth.usercredential.md#usercredential_interface}
102 */
103export const firebaseSignUp = (
104  email: string,
105  password: string
106): Promise<UserCredential> => {
107  return createUserWithEmailAndPassword(firebaseAuth, email, password);
108};
109
110/**
111 * Authenticates a user with the provided email and password using Firebase authentication.
112 *
113 * @param email - Account email.
114 * @param password - Account password.
115 * @returns a Promise that when resolved, returns a Firebase UserCredential object.
116 *
117 * [UserCredentail object documentation] - {@link https://firebase.google.com/docs/reference/js/auth.usercredential.md#usercredential_interface}
118 */
119export const firebaseLogin = (
120  email: string,
121  password: string
122): Promise<UserCredential> => {
123  return signInWithEmailAndPassword(firebaseAuth, email, password);
124};
125
126/**
127 * Sends a password reset email to the user associated to the provided email address.
128 *
129 * @param email - Email address associated to the user's account.
130 * @returns a Promise for the asynchronous operation that when resolved, returns nothing.
131 */
132export const firebasePasswordReset = (email: string): Promise<void> => {
133  return sendPasswordResetEmail(firebaseAuth, email);
134};
135
136/**
137 * Ends the current Firebase user session.
138 */
139export const firebaseLogout = async () => {
140  await signOut(firebaseAuth);
141};


The Signup Page

For the signup page, you are free to design the UI in any way you like, however, there are a couple of input fields that will be required for building the user's Donately fundraiser. These fields are:

  • First name
  • Last name
  • Email
  • Short bio/description of fundraising purpose
  • Fundraising goal

Once you have your UI and input fields set up, the next and most important step is handling the user submission. Your onSubmit function should be something along the lines of the code below:

Forms in the project were created using the react-hook-form package. Update the code according to your own implementation
1import { SubmitHandler } from "react-hook-form";
2import { createDonatelyFundraiser } from "../utils/donately";
3import { updateDonor, firebaseSignUp, firebaseAuth } from "../utils/firebase";
4
5// SignupFormInputs is a Typescript type declaring all the fields available in the signup form
6const onFormSubmission: SubmitHandler<SignupFormInputs> = async (
7    data,
8    event
9  ) => {
10    event?.preventDefault();
11    setLoading(true);
12    const {
13      email,
14      password,
15      confirmPassword,
16      firstName,
17      lastName,
18      fundraisingGoal,
19      purpose,
20      goalInKms,
21      acceptTOS,
22    } = data;
23    try {
24      // If user has not accepted the Terms of Service & Privacy Policy
25      if (!acceptTOS) {
26        // Set errors in any way you see fit
27        setTOSError(
28          "Please agree to the Terms of Service and Privacy Policy before proceeding."
29        );
30        return;
31      }
32      if (password === confirmPassword) {
33        // 1. Create a fundraiser on Donately for the user signing up
34        let donorData = await createDonatelyFundraiser(
35          firstName,
36          lastName,
37          email,
38          purpose,
39          Number(fundraisingGoal)
40        );
41        // 2. Sign up the user in Firebase with provided email and password
42        const userCredential = await firebaseSignUp(email, password);
43        // 3. Update the user's information in the database
44        const firebaseData: DonorDataType = {
45          firstName: firstName,
46          lastName: lastName,
47          // Donately id
48          donorId: donorData?.donorId,
49          purpose: purpose,
50          goalInKms: Number(goalInKms),
51          fundraisingGoal: Number(fundraisingGoal),
52          donately: {
53            campaignId: process.env.NEXT_PUBLIC_DONATELY_ACCOUNT_ID,
54            formId: donorData?.fundraiserFormId,
55            fundraiserId: donorData?.fundraiserId,
56          },
57        };
58        await updateDonor(
59          rootCollection,
60          firebaseData,
61          userCredential.user
62        );
63        
64        // 4. Redirect to the user's profile page on logic completion. 'router' is an instance of the 'useRouter' hook from 'next/router'
65        router.push(`/profile/${userCredential.user.uid}`);
66      } else {
67        // Handle error in any way you see fit
68        setPassError("Passwords do not match");
69        alert(
70          "Passwords do not match. Please update the fields and try again."
71        );
72      }
73    } catch (e: any) {
74      // Donately email validation error
75      if (
76        e.message
77          .toLowerCase()
78          .includes("email address did not pass validation.")
79      ) {
80        alert(
81          "An invalid email account was provided. Please change your email and try again."
82        );
83      } else if (e.message.toLowerCase().includes("auth/weak-password")) {
84        // Firebase password validation error
85        alert(
86          "Password is too weak. Passwords should be at least 6 characters long"
87        );
88      } else if (
89        e.message.toLowerCase().includes("auth/email-already-in-use")
90      ) {
91        // Firebase email already in use validation error
92        alert(
93          "There already seems to exist an account with the email provided. Try logging in or reset your password in the login page if you have forgotten your password."
94        );
95      } else {
96        // Default error message
97        alert(
98          "An error has occurred. Please try again. If the error persists, please get in touch with us."
99        );
100      }
101    } finally {
102      setLoading(false);
103    }
104  };

If you refer back to the Understanding the Architecture section of this tutorial, you'll see that the submission handler above should cover all the steps listed for the Signup page.


The Profile Update page

In a similar fashion to the signup page, there are no major UI requirements for your profile update other than the following fields:

  • First name
  • Last name
  • Short bio/description of fundraising purpose
  • Fundraising goal

In fact, it will save you a lot of time if you create a template for both pages and simply conditionally render the fields that you need/don't need to use.

Once again, handling the user submission will be the most important step. The code will be quite similar to what we have used on the signup page, the difference being that we are now updating data, not creating it:

1import { SubmitHandler } from "react-hook-form";
2import {
3  updateDonatelyFundraiser,
4  updateDonatelyUser,
5} from "../../../utils/donately";
6import {
7  firebaseAuth,
8  getDonorInfo,
9  updateDonor,
10} from "../../../utils/firebase";
11
12
13  // EditProfileFormInputs is a Typescript type declaring all the fields available in the update profile form
14  const onFormSubmission: SubmitHandler<EditProfileFormInputs> = async (
15    data,
16    event
17  ) => {
18    event?.preventDefault();
19    setLoading(true);
20    const {
21      firstName,
22      lastName,
23      fundraisingGoal,
24      purpose,
25      goalInKms,
26      subscribeToNewsletter,
27    } = data;
28    try {
29      // In our case, we make sure the id of the user updating
30      // the profile is the same as the id of the dynamic path
31      if (firebaseAuth?.currentUser?.uid === router.query.id) {
32        // Fetch user data from the backend (update path to your collection)
33        // Generally you will fetch userData in getStaticProps() so that you're able to pre-populate the page with the backend data
34        const userData = await getDonorInfo("PATH_TO_COLLECTION", router.query.id)
35      
36        // 1. Update donately donor profile with new information
37        if (
38          userData.donorId != null &&
39          (userData.firstName !== firstName || userData.lastName !== lastName)
40        ) {
41          await updateDonatelyUser(
42            userData.donorId,
43            firstName,
44            lastName
45          );
46        }
47        // 2. Update the user's Donately Fundraiser
48        if (
49          userData.donately?.fundraiserId != null &&
50          Number(fundraisingGoal) !== userData.fundraisingGoal
51        ) {
52          await updateDonatelyFundraiser(
53            userData.donately.fundraiserId,
54            Number(fundraisingGoal)
55          );
56        }
57        // 3. Update the user's Firebase information
58        const firebaseData = {
59          firstName: firstName,
60          lastName: lastName,
61          purpose: purpose ?? "",
62          goalInKms: Number(goalInKms),
63          fundraisingGoal: Number(fundraisingGoal),
64        };
65        
66        await updateDonor(
67          "PATH_TO_COLLECTION",
68          firebaseData,
69          firebaseAuth?.currentUser
70        );
71        // 4. Redirect to the user's profile page on logic completion
72        router.push(`/profile/${router.query.id}`);
73      } else {
74        router.push(`/`);
75      }
76    } catch (e) {
77      alert("An error occurred while updating your profile. Please try again.");
78    } finally {
79      setLoading(false);
80    }
81  };

Finalizing our implementation

Phew! That was a lot! At this point, we should now have a signup and profile update page that can update our backend and Donately with new user info.

But wait! How will viewers be able to donate? If you recall, from the Why Donately? section, I mentioned that

upgrading to the Starter tier (which we used for this project) gives you embeddable forms.

As such, these embeddable forms come in the form of an iframe. You can copy and paste the iframe code below wherever you find most appropriate (based on our implementation so far, that should be on your user's profile page). Through the src property of the iframe, you can provide specific donor information so that the correct donation form specific to your user will be loaded.

1<iframe src={`https://cdn.donately.com/core/5.6/donate-form.html?form_id=<USER_FORM_ID>&account_id=${process.env.NEXT_PUBLIC_DONATELY_ACCOUNT_ID}&stripe_key=${process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY}`} 
2	width="100%" 
3	height="1335px" 
4	frameborder="0" 
5	allowtransparency="true" allow="payment *" 
6	style="background-color: transparent; border: 0px none transparent; overflow: hidden; display: inline-block; visibility: visible; margin: 0px; padding: 0px; height: 1335px; width: 100%"></iframe>