{"id":18420822,"url":"https://github.com/ceceliacreates/ballot-cure","last_synced_at":"2026-04-24T16:04:37.992Z","repository":{"id":120378070,"uuid":"312737851","full_name":"ceceliacreates/ballot-cure","owner":"ceceliacreates","description":"An Ionic app using the Capacitor Community HTTP Plugin for native requests and file uploads.","archived":false,"fork":false,"pushed_at":"2020-11-29T22:53:39.000Z","size":1932,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-13T10:14:35.159Z","etag":null,"topics":["capacitor","ionic","mobile","tutorial"],"latest_commit_sha":null,"homepage":"","language":"CSS","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ceceliacreates.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-11-14T03:30:59.000Z","updated_at":"2020-11-29T22:53:41.000Z","dependencies_parsed_at":null,"dependency_job_id":"d14d7a8f-76ed-4281-829b-cee24e132fe4","html_url":"https://github.com/ceceliacreates/ballot-cure","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceceliacreates%2Fballot-cure","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceceliacreates%2Fballot-cure/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceceliacreates%2Fballot-cure/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ceceliacreates%2Fballot-cure/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ceceliacreates","download_url":"https://codeload.github.com/ceceliacreates/ballot-cure/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248695482,"owners_count":21146956,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["capacitor","ionic","mobile","tutorial"],"created_at":"2024-11-06T04:23:10.849Z","updated_at":"2026-04-24T16:04:37.933Z","avatar_url":"https://github.com/ceceliacreates.png","language":"CSS","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Building a Ballot Curing App with the Capacitor Community HTTP Plugin\n\n## What is the Capacitor Community HTTP Plugin?\n\nThe [HTTP plugin](https://github.com/capacitor-community/http) from the Capacitor Community is a versatile toolkit for native HTTP requests, file uploads/downloads, and cookie management.\n\nBy using native HTTP requests, developers prevent the dreaded, frustrating CORS issues that can happen when making requests from a browser. The HTTP plugin makes this easy by providing functions that make the request natively instead.\n\n## What we’ll learn\n\nIn this tutorial, we’ll be demonstrating two main features of the HTTP plugin:\n\n1. Native HTTP Requests (GET)\n2. File uploads\n\nThis tutorial is divided into four main sections.\n\n1. An overview of the functionality and structure of our demo application\n2. Building the native HTTP request functionality using the plugin\n3. Building the file upload functionality using the plugin\n4. Building the native application with Capacitor\n\nThe application and API code are open-sourced on GitHub:\n\n- [ballot-cure repository](https://github.com/ceceliacreates/ballot-cure)\n- [ballot-api repository](https://github.com/ceceliacreates/ballot-api)\n\n## What we’re building\n\nWe’ll be building a ballot curing app using the new Ionic Vue. Ballot curing became a hot issue in the recent U.S. election. When a problem occurs with an absentee ballot, voters must “cure” or resolve the issue by a deadline for their vote to count.\n\nThe functionality is similar to any system that requires a user to submit documents for verification. These are typical in financial, education, healthcare, and government applications where users may need to submit legal documents like a driver’s license or power of attorney.\n\n\u003e Note: If you like the idea of contributing to open-source projects that support government and civic programs, check out [Code for America](https://www.codeforamerica.org/).\n\n### App functionality\n\nOur application provides a simple interface for the following functionality:\n\n1. Search for a ballot by ID\n2. See results of ballot search, including the issue that needs to be resolved\n3. Allows user to upload a file to resolve the issue\n4. Submits the file for the corresponding ballot ID\n\n![video of app functionality](https://raw.githubusercontent.com/ceceliacreates/ballot-cure/main/BallotCureDemo.gif)\n\n## Building the ballot search with native HTTP requests\n\n### Installation and setup\n\nWe’ll use the Ionic CLI to create our Vue app with the blank starter, following the [Ionic Vue Quickstart](https://ionicframework.com/docs/vue/quickstart#creating-a-project-with-the-ionic-cli). This will also set up Capacitor so we can build to Android and iOS after building our app.\n\n```\nionic start ballot-cure blank --type vue\n```\n\nThen we cd into our project directory and run `ionic serve` to start up the development server for our app.\n\n### Building the search user interface\n\nTo focus on the functionality of the HTTP plugin, we’ll keep the UI simple with a single view. We can leverage components from the [Ionic Framework UI library](https://ionicframework.com/docs/components) to build our view quickly.\n\nLet’s start with the part of the application where the user searches for a ballot by ID. In our `Home.vue`, we will delete the content in the container `div` and replace it with text instructions, an input field, and a search button. We are using `v-model` to bind the input field to the `searchInput` data field.\n\n```javascript\n\u003cdiv class=\"ion-padding\"\u003e\n\u003cion-text color=\"primary\"\u003e\n    \u003ch2\u003eInstructions\u003c/h2\u003e\n\u003c/ion-text\u003e\n\u003cp\u003eEnter the ballot ID provided in your notice.\u003c/p\u003e\n\u003c/div\u003e\n\u003cion-item\u003e\n    \u003cion-label\u003eEnter Ballot ID\u003c/ion-label\u003e\n    \u003cion-input v-model=\"searchInput\" type=\"text\"\u003e\u003c/ion-input\u003e\n    \u003cion-button @click=\"search\"\u003eSearch\u003c/ion-button\u003e\n\u003c/ion-item\u003e\n```\n\nWe’ll also need an area to display the results of the ballot search. In an `ion-card` component, we’ll display the voter name, address, and ballot issue that are returned. We’ll use `v-if` to only display this component once we have a ballot, and wrap it in a `transition` component to fade in on appearance.\n\n```javascript\n\u003ctransition name=\"fade\"\u003e\n    \u003cion-card v-if=\"ballot.voterFirstName\"\u003e\n        \u003cion-card-header\u003e\n        \u003cion-card-title\u003eBallot Information\u003c/ion-card-title\u003e\n        \u003cion-card-subtitle\n                \u003e{{ ballot.voterFirstName }}\n                {{ ballot.voterLastName }}\u003c/ion-card-subtitle\n              \u003e\n        \u003cion-card-subtitle\u003e{{ ballot.voterAddress }}\u003c/ion-card-subtitle\u003e\n        \u003c/ion-card-header\u003e\n        \u003cion-card-content\u003e\n            \u003cion-text color=\"danger\"\n                \u003e\u003ch2\u003eIssue Requiring Resolution:\u003c/h2\u003e\u003c/ion-text\n              \u003e\n            \u003cp\u003e{{ ballot.issueDescription }}\u003c/p\u003e\n        \u003c/ion-card-content\u003e\n    \u003c/ion-card\u003e\n\u003c/transition\u003e\n```\n\nFinally, let’s update the name in the header and add an icon for styling. We’ll need to return our icon in the `setup()` function of our component definition.\n\n```javascript\n\u003cion-header :translucent=\"true\"\u003e\n    \u003cion-toolbar\u003e\n        \u003cion-title\u003e\n            \u003cion-icon :icon=\"shieldCheckmarkOutline\" size=\"large\"\u003e\u003c/ion-icon\u003eBallotCure\n        \u003c/ion-title\u003e\n    \u003c/ion-toolbar\u003e\n\u003c/ion-header\u003e\n```\n\n### Building the front-end logic\n\nNow let’s set up the logic for our component. We’ll use the Composition API built into Vue 3 to handle the component setup, data, watchers, and methods. There is an excellent breakdown of the API [in the Vue docs here](https://v3.vuejs.org/guide/composition-api-introduction.html#why-composition-api).\n\nFor the search functionality, we’ll need the following:\n\n- Data fields for the `searchInput`, `ballotId`, and `ballot`\n- A `search()` method to handle the button click, validate the input, and set the `ballotId`\n- A watch to trigger our `getBallot()` method whenever the `ballotId` is updated\n- A `getBallot()` method to get the ballot for the given `ballotId` and update our `ballot` data field\n- A helper `fetchBallot()` method to handle the API call associated with getting the ballot\n\nThis is what our code looks like inside our composition function:\n\ndata()\n\n```javascript\ndata() {\n    return {\n      searchInput: \"\",\n      ballotId: \"\",\n      ballot: {},\n      fileName: \"\",\n    };\n  },\n```\n\nwatch:\n\n```javascript\n  watch: {\n    ballotId(newBallotId, oldBallotId) {\n      this.getBallot(newBallotId);\n    },\n  },\n```\n\nmethods:\n\n```javascript\nmethods: {\n    async fetchBallot(id: string) {\n      const { Http } = Plugins;\n      const response = await Http.request({\n        method: \"GET\",\n        url: `https://hungry-brown-da828c.netlify.app/.netlify/functions/server/ballots/${id}`,\n      });\n      const ballot = response.data;\n      return ballot;\n    },\n    async getBallot(id: string) {\n      const newBallot = await this.fetchBallot(id);\n      this.ballot = newBallot;\n    },\n    search() {\n      if (this.searchInput.length === 5) {\n        const newBallotId = this.searchInput;\n        this.ballotId = newBallotId;\n      }\n    },\n    submit() {\n      console.log(this.fileName);\n    },\n  },\n```\n\n### Making requests to the API\n\nWe’ll be making requests to an Express API deployed as a [Netlify Function](https://docs.netlify.com/functions/overview/). As a reminder, you can see the code for the API [in the repository here](https://github.com/ceceliacreates/ballot-api). The deployed endpoint URL is:\n\n[https://hungry-brown-da828c.netlify.app/.netlify/functions/server/ballots](https://hungry-brown-da828c.netlify.app/.netlify/functions/server/ballots)\n\nOur app will make two requests to a single `/ballots/:id` endpoint:\n\n1. A `GET` that returns the ballot matching the passed ID\n2. A `POST` request that updates the ballot matching the passed ID (handled by our `HTTP.fileUpload()` function)\n\nNote: Because we aren’t updating a database, our data is not persistent. This Function is just for demonstration and to process our front-end request.\n\n### Bypassing CORS issues with native HTTP requests\n\nSo what happens when we make a request to the endpoint directly from an application running in the browser?\n\n![screenshot of a CORS error](https://raw.githubusercontent.com/ceceliacreates/ballot-cure/main/cors.PNG)\n\n😱\n\nCORS issues are particularly tricky because they normally cannot be resolved with front-end code. There’s [a great breakdown of CORS issues in the Ionic documentation](https://ionicframework.com/docs/troubleshooting/cors), but ultimately CORS issues can only be resolved in one of two ways -- change the API, or don’t make the request from the browser.\n\nIn our case, we could use the [Express cors middleware](https://expressjs.com/en/resources/middleware/cors.html) and add code like\n\n```javascript\nconst cors = require(“cors”)\napp.use(cors())\n```\n\nto our API to accept **all** requests from the browser, but this is not secure. Configuring our API to receive only the correct browser requests can be complex.\n\nTo resolve, let’s use a native request instead with the HTTP plugin.\n\n### Installing and configuring the HTTP Plugin\n\nFollowing the installation instructions for the plugin, we start by installing the package.\n\n```\nnpm install @capacitor-community/http\n```\n\nThen we can either use the Ionic CLI or npx to sync the plugin with Capacitor.\n\n```\nnpx cap sync\n```\n\nOR\n\n```\nionic cap sync\n```\n\n### Usage in our application\n\nWhen using the plugin for HTTP requests, you’ll need to import the package as well as Plugins from capacitor/core. In our application, we do this right in the script section of our Home view.\n\n```javascript\nimport \"@capacitor-community/http\";\nimport { Plugins } from \"@capacitor/core\";\n```\n\nThen, we destructure the plugin as close to its usage as possible. In our app, we do this in our `fetchBallot()` method that uses `HTTP.request()` to make the native request that returns our ballot information.\n\n```javascript\nasync fetchBallot(id: string) {\n    const { Http } = Plugins;\n    const response = await Http.request({\n        method: \"GET\",\n        url: `https://hungry-brown-da828c.netlify.app/.netlify/functions/server/ballots/${id}`,\n      });\n    const ballot = response.data;\n    return ballot;\n},\n```\n\nNow when we search for our ballot, the `GET` request will resolve like magic, because it’s being handled natively by the plugin.\n\n\u003e Note: The ballot-api repository has a version of the API that you can run locally with cors enabled for testing during development. You can access it on the cors-enabled branch.\n\nThe same function can be used for `POST` requests as well. For our application, we'll let the HTTP Plugin `fileUpload()` functionality handle the request for us.\n\n## File uploads with the HTTP Plugin\n\n### Adding the file selector and submit button\n\nLet’s add the elements we’ll need to our template. To better style our file selector, we’ll wrap the input in a `div` and use CSS to override the display. We are using `v-model` to bind this to a `fileName` data field. Because we have set a type of \"file\" on the `ion-input`, clicking the input will automatically open the system's file selector.\n\n```javascript\n\u003cdiv v-if=\"ballot.voterFirstName\"\u003e\n  \u003cdiv class=\"fileUpload ion-padding\"\u003e\n    \u003cspan\u003eClick to upload required document\u003c/span\u003e\n    \u003cion-input\n      type=\"file\"\n      name=\"file\"\n      v-model=\"fileName\"\n      class=\"upload\"\n    \u003e\u003c/ion-input\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\nWe also have a `div` that shows the selected file name (with the fakepath removed) that only appears once a file is selected. Our submit button triggers the `submit()` method on click.\n\n```javascript\n\u003ctransition name=\"fade\"\u003e\n    \u003cdiv v-if=\"fileName\"\u003e\n        \u003cp\u003e{{ fileName.slice(12) }}\u003c/p\u003e\n    \u003c/div\u003e\n\u003c/transition\u003e\n\u003cion-button class=\"ion-margin\" @click=\"submit\" size=\"large\"\u003eSubmit\u003c/ion-button\u003e\n```\n\nFor our logic, we’ll add our data field for `fileName` and write our `submit()` method.\n\n### Using the HTTP.fileUpload() method\n\n[Behind the scenes, the HTTP plugin submits the file as formData to our API](https://github.com/capacitor-community/http/blob/master/src/web.ts#L162). It submits the file uploaded, as well as the file name. You can see the [full list of options available on the fileUpload function here](https://github.com/capacitor-community/http/blob/master/src/definitions.ts#L69).\n\nInside our `submit()` method, we’ll use the `HTTP.fileUpload()` method and pass the URL of our endpoint as well as the `fileName`. The plugin function will submit the file as a `POST` request to our endpoint.\n\n```javascript\nasync submit() {\n    const { Http } = Plugins;\n    const ret = await Http.uploadFile({\n        url: `https://hungry-brown-da828c.netlify.app/.netlify/functions/server/ballots/${this.ballotId}`,\n        name: this.fileName,\n        filePath: this.fileName,\n});\n},\n```\n\n### Handling the uploaded file in our API\n\nIn our example, we process this file using an [Express multer middleware.](http://expressjs.com/en/resources/middleware/multer.html) We can then set this file as a value in our ballot.\n\n```javascript\n// middleware to handle the file upload\nconst multer = require(\"multer\");\nconst upload = multer({});\n\n// Express can access the submitted file on the request body\nrouter.post(\"/ballots/:id\", upload.any(), function(req, res) {\n  const ballotId = req.params.id;\n  const ballotIndex = ballots.findIndex((ballot) =\u003e ballot.id === ballotId);\n  const ballotToUpdate = ballots[ballotIndex];\n  if (req.body) {\n    ballotToUpdate.issueResolutionFile = req.body;\n    res.json(ballotToUpdate);\n  } else {\n    const error = {\n      message: \"file required\",\n    };\n    res.json(error);\n  }\n});\n```\n\nFor additional security, we could require the user enter a PIN that we validate on our API before updating.\n\nCurrently, the `HTTP.fileUpload()` function returns an empty object, but in future versions we could use the `POST` request response to confirm the upload was successful for the user.\n\n## Building our native application\n\nOnce we’ve completed the source code for our application, we can build our application for Android and iOS using the [Capacitor setup instructions here in the Ionic documentation](https://ionicframework.com/docs/vue/your-first-app/6-deploying-mobile#capacitor-setup).\n\nFirst, run `ionic build` and then add the projects.\n\n```\nionic cap add ios\nionic cap add android\n```\n\n#### Add to Android Main Activity\n\nFor Android, you’ll need to import and register the plugin in your `MainActivity` file. In our application, this is in `android/app/src/main/java/io/ionic/starter/MainActivity.java`.\n\nThis file is generated when we build the Ionic app, so ensure you register the plugin after completing and building the source code.\n\n```java\nimport com.getcapacitor.plugin.http.Http;\n\npublic class MainActivity extends BridgeActivity {\n@Override\npublic void onCreate(Bundle savedInstanceState) {\n    super.onCreate(savedInstanceState);\n    // Initializes the Bridge\n    this.init(savedInstanceState, new ArrayList\u0026lt;Class\u0026lt;? extends Plugin\u003e\u003e() {{\n      // Additional plugins you've installed go here\n      // Ex: add(TotallyAwesomePlugin.class);\n      add(Http.class);\n    }});\n}\n}\n```\n\nYou can then open the project and run the application.\n\n## Going further with HTTP Plugin\n\nWe’ve outlined in detail how to use native HTTP requests and file uploads with the plugin. In addition, you can also:\n\n1. Download files\n2. Manage cookies\n\nIf we wanted to demonstrate these with our application, we could add functionality to download a PDF of instructions, or use cookies to save the ballot ID entered by the user for persistence. These would make great open-source contributions to this project! 😉\n\nYou can do a lot or a little with the HTTP Plugin, so it’s a good fit for a variety of projects that need native HTTP requests, file upload and downloads, and cookie management.\n\nFor more information, check out the [plugin repository here](https://github.com/capacitor-community/http), and the code for [this tutorial application here](https://github.com/ceceliacreates/ballot-cure).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fceceliacreates%2Fballot-cure","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fceceliacreates%2Fballot-cure","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fceceliacreates%2Fballot-cure/lists"}