TutorialsAPI call

There are two ways to make api calls based on our preference:

  • Through API Routes defined in /server
  • Through cloud functions defined in /functions

The firebase-admin-sdk does not handle firestore calls well through an SSR app. Thus you will notice the firebase SDK was used instead here.

We have left it up to the developer to decide the way they want to go. Below are the guidelines for handling calls that are defined as API routes in /server folder.

The code is identical in handling it with cloud functions.

API Calls

Any file in /server/{handler}.ts in the / folder are API endpoints.

Protected API calls

Firebase handles verification of tokens through their SDK. Just make a normal API call on the front-end like this:

/app/services/auth/auth.service.ts

...
  export class AuthService {
  user: firebase.User | null;

  constructor(private afAuth: AngularFireAuth) {}

  async getIdToken() {
    const user = await this.afAuth.currentUser;

    if (user) {
      return user.getIdToken();
    } else {
      throw new Error('User not authenticated');
    }
  }
  ...

/app/services/api/api.service.ts

...
@Injectable({
  providedIn: 'root',
})
export class ApiService {
  constructor(
    private http: HttpClient,
    private authService: AuthService,
  ) {}


  async createBillingPortal(data: { returnUrl: string }) {
    const idToken = await this.authService.getIdToken();

    return this.http
      .post<{
        url: string;
      }>(`/api/stripe/create-billing-portal`, data, {
        headers: { authorization: `Bearer ${idToken}` },
      })
      .toPromise();
  }

  ...

/app/pages/home.component.html

<button class="btn btn-primary" (click)="handleBilling()">
  <span *ngIf="isLoading" class="loading loading-spinner loading-sm"></span>
  Purchase
</button>

/app/pages/home.component.ts

export class HomeComponent {
  constructor(private apiService: ApiService) {

  }

  handleBilling() {
    try {
       await this.apiService.createBillingPortal({returnUrl: 'https://shipangular.com'})
    }catch(err){
      // handle error
    }
  }

The API file should look like this:

/server/stripe.ts

router.post('/create-checkout-session', bodyParser.json(), async (req, res) => {
  const token = req.headers.authorization?.split('Bearer ')[1];

  if (!token) {
    return res.status(401).send('Unauthorized');
  }

 let decodedToken;
  try {
    decodedToken = await admin.auth().verifyIdToken(idToken);
  } catch (error) {
    console.log('Failed to decode the ID token: ', error);

    res.status(401).send({ message: 'TOKEN_EXPIRED' });
    return;
  }

  try {
    const userId = decodedToken.uid;


  const { priceId, successUrl, cancelUrl } = req.body;

  if (!priceId) {
    res.status(400).send({ message: 'Price ID is required' });
    return;
  } else if (!successUrl || !cancelUrl) {
    res.status(400).send({ message: 'Success and cancel URLs are required' });
    return;
  }

  const stripeSession = await stripe.checkout.sessions.create({
    mode: 'payment',
    line_items: [
      {
        price: priceId,
        quantity: 1,
      },
    ],
    discounts: couponId
      ? [
          {
            coupon: couponId,
          },
        ]
      : [],
    customer_creation: 'always',
    client_reference_id: userId,
    success_url: successUrl,
    cancel_url: cancelUrl,
    tax_id_collection: { enabled: true },
  });

  return res.status(200).send({ url: stripeSession.url });
});

Going to production?

  • Navigate to the /functions folder
  • run npm run deploy in your firebase project
  • Update the endpoints in your Stripe Webhooks
  • Update the apiBaseUrl in your /environments/environment.ts