Bypassing CORS with the Capacitor Community HTTP Plugin
This is a guest post from Tessa. Tessa enjoys drawing and eating snacks, and currently works as a developer experience engineer. She frequently speaks about Vuejs and is a host on the Enjoy The Vue podcast.
CORS, or Cross-Origin Resource Sharing, is not a popular word among developers working with front-end tech. A browser-enforced restriction mainly to protect users from a type of attack known as cross-site request forgery, CORS is more well known for the headaches it causes web developers, and that’s before we even think about mentioning mobile applications! But what if there were another way?
Thanks to the Capacitor Community, there is! The Capacitor Community is an open-source working group that builds and maintains useful tools and plugins for Ionic’s Capacitor runtime, and today we’ll be looking at how to use its HTTP plugin to sidestep CORS and smoothly make successful HTTP requests across desktop and mobile devices.
Let’s use the plugin to power an advice-a-day app to familiarize ourselves with its inner workings and API.
What we’ll build
A Vue.js app loads and displays one cross-stitched piece of advice per day from the Advice Slip JSON API and can be deployed cross platform.
What we’ll learn
- How to make a GET request with the HTTP plugin
- How to get, set, and delete cookies
Prior knowledge
In order to follow along, it might be helpful to have some understanding of the following technologies:
Getting started
You can either clone the startingPoint branch or start your own project from scratch with the following steps:
- Create a new Ionic Vue project as per Creating a project with the Ionic CLI
cd
into your project and install the HTTP plugin with the following commands:
npm install @capacitor-community/http
npx cap sync
😬 Tip
If, after running both commands, you see the error
Capacitor could not find the web assets directory "pathToYourRepo/dist"
, try running
yarn build
or npm build
and before npx cap sync
a second time.
- Open your project in your favorite code editor; I like to use VSCode.
- Optional: Download HovdenStitch, a free cross-stitch font, and move the
otf
file to yourpublic/assets
folder asHovdenStitchRegular.otf
. Then insrc/App.vue
, copy and paste the below style tag at the bottom of the file to allow this custom font to be used across your app.
<style>
@font-face {
font-family: 'HovdenStitch';
src: url('../public/assets/HovdenStitchRegular.otf');
}
</style>
💡 Tip:
Although for the purposes of this tutorial CSS styles will be contained within Vue’s
Single File Components for clarity, when it comes to your own projects, consider putting global styles in your
src/theme folder
.
Designing the order of operations
The basic idea for this app’s functionality is that it will fetch and display a max of one piece of advice per 24 hours, regardless of whether the user refreshes the page or leaves the app, and erase the current advice approximately one hour before new advice is fetched.
While this concept may seem simple on its face, it requires quite a bit of state-tracking in order to ensure the expected behavior, and therefore you might be helpful to draw or write out a rough plan for how you expect things to go as I’ve done here:
Preparing HTTP helper functions
In
src/views/Home.vue
, replace your script
tag with the below starter code:
<script>
import { defineComponent } from 'vue';
import { IonPage } from '@ionic/vue';
export default defineComponent({
name: 'Home',
components: { IonPage },
data: () => ({
advice: '', // Advice currently being displayed
animationState: '', // Dynamic class that determines whether to fade advice in or out
hourToFetchNewAdvice: null,
lastSaveDate: null, // The last time we fetched new Advice
today: new Date(),
}),
computed: {
currentDate() {
return this.today.getDate();
},
currentHour() {
return this.today.getHours();
},
hourToEraseCurrentAdvice() {
let oneHourPrior = this.hourToFetchNewAdvice - 1;
if (oneHourPrior < 0) oneHourPrior = 23;
return oneHourPrior;
},
},
// When we'll check if advice data needs to be changed
async ionViewWillEnter() {
// For more on`ionViewWillEnter`
// see: https://ionicframework.com/docs/vue/lifecycle#guidance-for-each-lifecycle-method
},
methods: {},
});
</script>
Optional: Move the
script
tag to be at the top of the file, above thetemplate
tag as per the Vue Style Guide. Once you get used to this pattern it can speed up your development process by reducing the scrolling betweenscript
andtemplate
and betweentemplate
andstyle
.
The first thing we’ll need to do is import our HTTP plugin by adding the following code to the top of the
script
tag as per the
README:
import '@capacitor-community/http';
import { Plugins } from '@capacitor/core';
const { Http } = Plugins;
Now we have access to the HTTP plugin and its helper methods inside this component, so we can add the following functions to our component’s
methods
:
async fetchAdvice() {
return await Http.request({
method: 'GET',
url: 'https://api.adviceslip.com/advice',
})
.then(({ data }) => {
// Set dynamic class to fade in text
this.animationState = 'fadeIn'
this.resetAnimationState()
// Save new advice
this.advice = JSON.parse(data).slip.advice.toUpperCase()
// In other words:
// const dataInJs = JSON.parse(data)
// const slip = dataInJs.slip
// this.advice = slip.advice
// this.advice = this.advice.toUpperCase() — // font supports upper case only
// Update lastSaveDate
this.lastSaveDate = this.currentDate
})
},
updateAdvice() {
// If 24h have passed, fetch new advice
if(this.currentHour === this.hourToFetchNewAdvice && this.currentDate != this.lastSaveDate) this.fetchAdvice()
// If 23 hours have passed, start fading out current advice
else if (this.currentHour === this.hourToEraseCurrentAdvice && this.advice) {
// Set dynamic class to fade out text
this.animationState = 'fadeOut'
this.resetAnimationState()
// Clear advice from state after the fade out animation ends
setTimeout(() => {
this.advice = ''
}, 10000)
}
// Check every 10m if it's time to fetch/erase advice
setTimeout(this.updateAdvice, 600000)
},
// Clear animation from advice after one playthrough
// A safer approach might be to listen for `transistionend`
resetAnimationState() {
setTimeout(() => {
this.animationState = ''
}, 10000)
}
The fetchAdvice
method is almost identical to the
GET
example in the HTTP Plugin’s README, but its syntax has been reordered to fit within Vue’s
methods
style. The
HTTP.request
method allows us to make HTTP calls across different deploy targets without worrying about CORS issues. Here we’re using it to request advice from the
Advice Slip JSON API’s random advice GET endpoint.
Next, we’ll want to add the following code to ionViewWillEnter
to fetch advice when the page loads:
async ionViewWillEnter() {
// If we haven't stored an hourToFetchNewAdvice before, calculate and store that and hourToEraseCurrentAdvice
if(!this.hourToFetchNewAdvice) this.hourToFetchNewAdvice = this.currentHour
this.updateAdvice()
},
Displaying our cross-stitched advice
Open views/Home.vue
and replace the existing
template
with the below code:
<template>
<IonPage>
<div class="Home">
<img src="yourImageHere" alt="Don't forget to add alt text!" />
<p class="embroidery" :class="animationState">{{ advice }}</p>
<img src="yourImageHere" alt="Don't forget to add alt text!" />
</div>
</IonPage>
</template>
We won’t need to mess with this too much going forward, but let’s briefly about what’s happening here:
- This view, or page, is wrapped in the
IonPage
component to enable us to leverage component lifecycle hooks. - The
Home
class will be used to visually center and style advice in the viewport. - The
img
tags are placeholders for you to add personalized decorative flourishes to surround your advice; feel free to also copy the images from the base repo or remove them altogether. - The
p
tag is where we’ll render advice from the Advice Slip JSON API. - The v-bound
animationState
class enables us to dynamically fade advice in and out as necessary.
Next, replace the style
tag with the following:
<style scoped>
/* Center and style the content */
.Home {
background: white;
height: 100%;
padding: 1rem;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
/* Apply the custom font and style the text */
font-family: 'HovdenStitch';
font-size: 5rem;
color: #002657;
text-align: center;
}
/* Allow the flourishes to visually curve more closely around the text */
.embroidery {
max-width: 686px;
position: absolute;
top: 53%;
transform: translateY(-75%);
}
/* Animate new advice being added and old advice being removed */
.fadeIn {
animation: fadeIn ease 10s;
}
.fadeOut {
animation: fadeOut ease 10s;
}
@keyframes fadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
</style>
Now if you run yarn serve
or npm serve
, you should be able to see some advice in your local preview in your browser!
Persisting state
While it’s great that our advice is rendering and all, you may have noticed a small catch: we get a new piece of advice on every page load, even though our
updateAdvice
method is supposed to wait 24 hours before fetching new advice. This is because our state is stored only within the component, which means when the component disappears, so does our data.
To get around this, we’ll store some of our state in cookies, which can outlive the component lifecycle.
To get started, add the following helpers to the component’s methods
:
async setCookie(optionsObject) {
const cookie = await Http.setCookie({
...optionsObject,
ageDays: 2, // Set max number of days to save cookie
})
},
This method is very similar to the setCookie
example in the HTTP Plugin’s README, but its syntax has been reordered to fit within Vue’s
methods
style. It has also been augmented to expire any cookies after two days.
One potential point of confusion here is that when looking at the Http.setCookie source code, it may seem like this method is behaving essentially identically to the
browser’s HTTP Set-Cookie approach; however, if you try to pass in a value for
name
or SameSite
, you will quickly discover this is not so. The
method’s interface reveals that this method will only take four potential pieces of data:
url
, key
,
value
, and ageDays
, where key
becomes the cookie’s name
, and
ageDays
its Expires
value.
async deleteCookie(optionsObject) {
return await Http.deleteCookie(optionsObject)
},
This method is similar to the deleteCookie
example in the HTTP Plugin’s README, with the addition of an optional
optionsObject
argument which can be passed to the HTTP Plugin’s
deleteCookie
method. If we check out
the interface for this method in the source code, we can see that it will accept either a
key
or a url
to specify which cookie should be deleted.
async getCookie(key) {
const allCookiesWrapper = await Http.getCookies()
const allCookies = allCookiesWrapper.value
for (let i = 0; i < allCookies.length; ++i) {
const currentCookie = allCookies[i]
if(currentCookie.key === key) return currentCookie.value
}
return null
},
This method is similar to the
getCookies
example in the HTTP Plugin’s README. However, that method will return an Array of all cookies, so if we want to get a specific one we’ll have to filter the list ourselves.
Note that forEach
will not work here as the loop cannot be short-circuited by a return
statement.
Now that we have access to some cookie helper methods, let’s use them.
First, add them to ionViewWillEnter
:
async ionViewWillEnter() {
// Check if there are a stored date, hour, and advice in cookies, i.e. outside component/session state
await Promise.all([
this.getCookie('lastSaveDate').then(lastSaveDate => this.lastSaveDate = +lastSaveDate),
this.getCookie('hourToFetchNewAdvice').then(hourToFetchNewAdvice => this.hourToFetchNewAdvice = +hourToFetchNewAdvice),
this.getCookie('advice').then(advice => this.advice = advice)
])
if(!this.hourToFetchNewAdvice) this.hourToFetchNewAdvice = this.currentHour
// Store the hourToFetchNewAdvice in cookies regardless so it doesn't expire
this.setCookie({
key: 'hourToFetchNewAdvice',
value: this.hourToFetchNewAdvice,
})
this.updateAdvice()
},
Next, update fetchAdvice
:
async fetchAdvice() {
return await Http.request({
method: 'GET',
url: 'https://api.adviceslip.com/advice',
})
.then(({ data }) => {
this.animationState = 'fadeIn'
this.resetAnimationState()
this.advice = JSON.parse(data).slip.advice.toUpperCase()
// Save the advice to a cookie, too
this.setCookie({
key: 'advice',
value: this.advice,
})
this.lastSaveDate = this.currentDate
// Update lastSaveDate in cookies also
this.setCookie({
key: 'lastSaveDate',
value: this.currentDate,
})
})
},
And updateAdvice
:
updateAdvice() {
if(this.currentHour === this.hourToFetchNewAdvice && this.currentDate != this.lastSaveDate) this.fetchAdvice()
else if (this.currentHour === this.hourToEraseCurrentAdvice && this.advice) {
this.animationState = 'fadeOut'
this.resetAnimationState()
setTimeout(() => {
this.advice = ''
// Erase the advice from cookies as well
this.deleteCookie({ key: 'advice' })
}, 10000)
}
setTimeout(this.updateAdvice, 600000)
},
Celebrate!
We did it! Now the app will fetch and erase advice once every 24 hours as designed. 🎉
Review
We covered a lot of concepts from the Capacitor Community HTTP Plugin today including:
- How to make an HTTP
GET
request - How to save a cookie
- Which properties of a cookie can be set with the plugin
- How to get a specific cookie
- How to delete a specific cookie
- How to use the source code to answer questions not covered by the README
Next steps
What’s next in the exciting world of cross-platform Vue apps? If you’re short on ideas, here’s a few suggestions for next steps
- Offline first: Try persisting state using another method, such as service-worker caching
- PWA/mobile: Try deploying the app to your phone as a Progressive Web App or a native
Android/iOS one
- Note: If you opted to generate your own project for this tutorial instead of cloning from the repo, you may need to follow some additional installation steps to get the HTTP Plugin working on Android
- Loading state: How might you save the state of the app such that if the user exits while advice is fading in or out, the transition will resume at the same spot tnext time they reopen the app?
- Another API: There are lots of free APIs out there; how would you apply the HTTP Plugin to a new project?
🎉 Happy coding! 🎉