It took about 2 working weeks to finish the app. I made 4 iterations of the app, and each duration mentioned on each step reflects this. Most of my time went into styling the application since I opted to use raw CSS. The hosted solution can be found here: https://expensify.fly.dev/
The project was quite simple to start since the project development steps were pretty much predetermined.
Step 1: Hosting basic PHP environment
Duration: Approximately 2 hours
Since I’m a MacOS user, installing PHP was quite straightforward:
# don't skip this otherwise, you'll be stuck for hours trying to figure out what the bash errors mean. 😢
brew update
brew install
I used the PHP Server VScode extension to serve my app and PHP DEVSENSE VScode extension for PHP development tools like code completion.
Step 2: Understanding Tech Stack constraints and building PHP proxy file.
Duration: Approximately 4 days
I liked that my choices were narrowed down for me. Otherwise, I would’ve been paralyzed by the tons of choices not only in the JavaScript ecosystem but also in PHP.
This is the tech stack I used for the app:
- HTML - structure and layout.
- CSS - styling.
- PHP - build a simple proxy file.
- JavaScript.
- jQuery - dynamic interactivity.
- Ajax - make API requests.
Building the PHP proxy file took longer than I’d liked:
// proxy.php
<?php
// 1.
$url = $_REQUEST["url"];
if (!$url) {
echo "You need to pass in a target URL.";
return;
}
// 2.
$response = "";
switch (getMethod()) {
case 'POST':
$response = makePostRequest(getPostData(), $url);
break;
case 'GET':
$response = makeGetRequest(getGetData($url), $url);
break;
default:
echo "This proxy only supports POST AND GET REQUESTS.";
return;
}
echo $response;
function getMethod()
{
return $_SERVER["REQUEST_METHOD"];
}
// 3.
function makePostRequest($data, $url)
{
$httpHeader = array(
'Content-Type: application/json',
'Content-Length: ' . strlen($data)
);
return makePostCurl('POST', $data, true, $httpHeader, $url);
}
// 4.
function makeGetRequest($data, $url)
{
$ch = initCurl($url);
curl_setopt($ch, CURLOPT_URL, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
// 5.
function getPostData()
{
return http_build_query($_POST);
}
// 6.
function getGetData($url)
{
$daurl = http_build_query($_GET);
$daurl = urldecode($daurl);
return substr($daurl, 4);
}
// 7.
function initCurl($url)
{
$httpHeader = array(
'Content-Type: application/x-www-form-urlencoded'
);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeader);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36');
return $ch;
}
// 8.
function makePostCurl($type, $data, $returnTransfer, $httpHeader, $url)
{
$ch = initCurl($url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $type);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, $returnTransfer);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
?>
- Retrieves the target URL from the query string parameter,
url. - Determines the request method of the current HTTP request through the
getMethod()helper function. - If it’s a
POSTrequest, it calls themakePostRequest()function, which sends thePOSTdata to the target URL using cURL. - If it’s a
GETrequest, it calls themakeGetRequest()function, which sends theGETdata to the target URL using cURL. - The
getPostData()function retrieves thePOSTdata as a URL-encoded string. - The
getGetData()function retrieves theGETdata as a string and removes the initial “data=” substring. - The
initCurl()function initializes a cURL handle with the URL and sets the HTTP headers and user agent. The response from the target URL is returned and printed to the client’s browser. - The
makePostCurl()function creates a cURL handle, sets the request method to POST, sets the POST data, sets the return transfer option, sets the HTTP headers, and executes the request.
Step 3: Check if auth token is present in the cookie storage
Duration: Approximately 6 hours
On page load, there’s a check to confirm if the authToken cookie exists. If one exists, the user views the transactions page (step 6), if not, the user is presented with a login form.
<!-- index.php -->
<body>
<!-- Show login form if authToken cookie is expired -->
<?php
if (!isset($_COOKIE['authToken'])) {
include "login.php";
} else {
include "transactions.php";
}
?>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript" src="script.js"></script>
</body>
Step 4: Creating Log in form
Duration: Approximately 10 hours
The login form reads your email and password information.
<!-- login.php -->
<body>
<div id="login-content" class="login-form">
<!-- Login Form-->
<form method="POST" id="login-form">
<h1>
Login
</h1>
<div class="content">
<div class="input-field">
<input type="email" placeholder="Email" id="user-email" value="expensifytest@mailinator.com"
required />
</div>
<div class="input-field">
<input type="password" placeholder="Password" id="user-password" value="hire_me" required />
</div>
<a href="#" class="link">Forgot Your Password?</a>
</div>
<div class="action">
<button id="cancel-login">Cancel</button>
<button type="submit">Login</button>
</div>
</form>
</div>
<script type="text/javascript" src="script.js"></script>
<script>
// handle click on login cancel button
$("#cancel-login").click((e) => {
e.preventDefault()
$("#login-form").trigger("reset");
})
</script>
</body>
// script.js
$(document).ready(() => {
// submit user login info
$('#login-form').submit((e) => {
e.preventDefault();
// 1.
const partnerName = "applicant";
const partnerPassword = "d7c3119c6cdab02d68d9";
let partnerUserID = $('#user-email').val();
let partnerUserSecret = $('#user-password').val();
//store all the submitted data in a string.
let formData = 'partnerName=' + partnerName + '&partnerPassword=' + partnerPassword + '&partnerUserID=' + partnerUserID + '&partnerUserSecret=' + partnerUserSecret;
let authToken, jCode
let url = "proxy.php?url=https://www.expensify.com/api?command=Authenticate"
const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g;
// 2.
if (!partnerUserID.match(emailRegex)) {
alert("Please Enter a Valid Email!");
return false;
}
// 3.
else {
$.ajax(url, {
type: "POST",
data: formData,
dataType: "json",
}).done((res) => {
authToken = res.authToken
jCode = res.jsonCode
//4.
if (jCode == 200) {
document.cookie = `authToken=${authToken}; max-age=3600; `
alert("⚠️ Reload this window to view all your transactions.")
} else {
ErrorHandler(jCode, res.message)
}
})
}
});
});
When the login form is submitted:
- API and user account credentials are captured and stored as a query string.
- Checks of the user’s email is valid.
- AJAX makes a
POSTrequest to theAuthenticateendpoint. - If the request is successful, the authToken retrieved is set in cookie storage. If not, the
ErrorHandlerfunction is called for the next error-handling step to execute.
Step 5: Handle Error messages
Duration: Approximately 2 hours
The ErrorHandler function outputs meaningful alert messages for any errors received should an API request fail.
// script.js
// handle common error codes
const ErrorHandler = (jCode, msg) => {
let message = ""
switch (jCode) {
case 401:
message = 'Wrong Password! Please enter the correct password.'
break
case 404:
message = 'Account not found! Please enter a valid email.'
break
case 407:
message = "Your session has expired. Please sign in again."
break
default:
message = msg
}
alert(message)
}
Step 6: Fetch all transactions
Duration: Approximately 6 hours
Transactions are fetched and added to the table body.
<!-- transactions.php -->
<div id="transaction-table" class="transaction-table">
<div class="transaction-head">
<h2>Transactions:</h2>
<button role="button" id="create-transaction-btn">+ Create Transaction</button>
</div>
<table>
<thead>
<tr>
<th>Transaction Date</th>
<th>Merchant</th>
<th>Amount ($)</th>
</tr>
</thead>
<!-- transaction rows are added here -->
<tbody id="transaction-table-body">
</tbody>
</table>
</div>
// script.js
// Hydrate transaction table with data
// 1.
const BuildTransactionTable = (data) => {
let url = "proxy.php?url=https://www.expensify.com/api?command=Get"
let table = $('#transaction-table-body')
let transactions, jCode
// 2.
$.ajax(url, {
data,
dataType: "json",
}).done((res) => {
transactions = res.transactionList
jCode = res.jsonCode
// 3.
if (jCode == 200) {
for (let i = 0; i < transactions.length; i++) {
let transactionCost = parseFloat(transactions[i].amount / 100).toFixed(2)
transactionCost = new Intl.NumberFormat('en-US', { currency: 'USD' }).format(transactionCost)
let row = `<tr>
<td>${transactions[i].created}</td>
<td>${transactions[i].merchant}</td>
<td>${transactionCost}</td>
</tr>`
table.append(row)
}
} else {
ErrorHandler(jCode, res.message)
}
});
}
The BuildTransactionTable function handles how the transaction table rows are generated.
- Receives
dataargument that’s the data query string required to make a request. - Makes a
GETrequest using AJAX to theGetendpoint. - If the request is successful, it populates the transaction table with rows of all transaction data. Otherwise, the
ErrorHandlerfunction displays an error message.
Step 7: Manipulating transactions displayed in the table
Duration: Approximately 5 hours
The transactions fetched from the previous step are a hustle to go through. Therefore, I added a simple input form on top of the table to help navigate the transactions.
<!-- transactions.php -->
<div id="transaction-table" class="transaction-table">
<div class="transaction-head">
<h2>Transactions:</h2>
<button role="button" id="create-transaction-btn">+ Create Transaction</button>
</div>
<!-- Simple form to control transaction rows displayed -->
<form id="show-form">
<table>
<tbody>
<tr>
<td>
<label for="start-date">From:</label>
<input id="start-date" required type="date" name="start-date" placeholder="Start Date" />
</td>
<td>
<label for="end-date">To:</label>
<input id="end-date" required type="date" name="end-date" placeholder="End Date" />
</td>
<td>
<button type="submit" id="show-button">Show</button>
<button type="button" id="show-all">Show All</button>
<button type="button" id="cancel-show">Cancel</button>
</td>
</tr>
</tbody>
</table>
</form>
<table>
<thead>
<tr>
<th>Transaction Date</th>
<th>Merchant</th>
<th>Amount ($)</th>
</tr>
</thead>
<tbody id="transaction-table-body">
</tbody>
</table>
</div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript" src="script.js"></script>
<script>
// Handle Create Transaction modal
let modal = $("#create-transaction");
let btn = $("#create-transaction-btn");
let span = $(".close");
let cancel = $("#cancel-btn")
// open the modal
btn.click(() => {
modal.css("display", "block")
})
// close the modal
span.click(() => {
modal.css("display", "none")
})
cancel.click(() => {
modal.css("display", "none")
})
// When the user clicks anywhere outside of the modal, close it
$(document).click((e) => {
if ($(e.target).is('#create-transaction')) {
modal.css("display", "none")
}
})
</script>
// script.js
// fetch all transactions to populate the transactions table
const GetTransactions = () => {
let table = $('#transaction-table-body')
let showBtn = $("#show-button")
let showAllBtn = $("#show-all")
let cancelShowBtn = $("#cancel-show")
let transactionParams = cookieToken + '&returnValueList=transactionList'
// 1.
BuildTransactionTable(transactionParams)
// 2.
showBtn.click((e) => {
e.preventDefault()
let startDate = $("#start-date").val()
let endDate = $("#end-date").val()
table.empty()
if (startDate != null || startDate != undefined || endDate != null || endDate != undefined) {
transactionParams = cookieToken + '&returnValueList=transactionList' + '&startDate=' + startDate + '&endDate=' + endDate
BuildTransactionTable(transactionParams)
} else {
transactionParams = cookieToken + '&returnValueList=transactionList'
BuildTransactionTable(transactionParams)
}
})
// 3.
showAllBtn.click((e) => {
e.preventDefault()
table.empty()
transactionParams = cookieToken + '&returnValueList=transactionList'
BuildTransactionTable(transactionParams)
})
// 4.
cancelShowBtn.click((e) => {
e.preventDefault()
$("#show-form").trigger("reset");
})
}
- The function calls the
BuildTransactionTablefunction by default to populate the transactions table with all transactions fetched. - When the
Showbutton is clicked, it only displays transactions between the specified start and end date. - When the
Show Allbutton is clicked, it displays all transactions. - When the
Cancelbutton is clicked, it resets the date input fields.
Step 8: Create a new transaction form
Duration: Approximately 2 hours
The + Create Transaction button on top of the transactions table displays a form modal to create a new transaction.
<!-- transactions.php -->
<div class="login-form modal" id="create-transaction">
<!-- Create transaction form here -->
<form method="POST" id="transaction-form" class="modal-content">
<div class="modal-header">
<h1>Create Transaction</h1>
<span class="close">×</span>
</div>
<div class="content">
<div class="input-field">
<input type="date" placeholder="Transaction date" id="transaction-date" required />
</div>
<div class="input-field">
<input type="string" placeholder="Merchant" id="merchant" required />
</div>
<div class="input-field">
<input type="number" placeholder="Amount" id="transaction-amount" required />
</div>
</div>
<div class="action">
<button id="cancel-btn">Cancel</button>
<button type="submit">Add</button>
</div>
</form>
</div>
// script.js
$(document).ready(() => {
// create transaction form
$('#transaction-form').submit((e) => {
e.preventDefault();
let transactionDate = $('#transaction-date').val();
let transactionAmount = $('#transaction-amount').val();
let merchant = $('#merchant').val();
let url = "proxy.php?url=https://www.expensify.com/api?command=CreateTransaction"
let createTransaction = cookieToken + '&created=' + transactionDate + '&amount=' + transactionAmount + '&merchant=' + merchant;
let jsCode
// 1.
$.ajax(url, {
type: "POST",
data: createTransaction,
dataType: "json",
}).done((res) => {
jsCode = res.jsonCode
// 2.
if (jsCode == 200) {
alert("Transaction Saved ✅")
$(".alert").css("display", "block")
$("#create-transaction").css("display", "none")
// 3.
} else {
ErrorHandler(jsCode, res.message)
}
});
})
});
- When the create transaction form is submitted, it sends a
POSTrequest using AJAX to theCreateTransactionendpoint for the new transaction to be added. - If the request is successful, the function sends an alert that the transaction has been saved. And also, notifies the user to reload their window to display the updated transactions (step 9).
- If the request fails, the
ErrorHandlerfunction displays a useful error message.
Step 9: Prompt user to refresh page
Duration: Approximately 2 hours
To prove that a new transaction was created, the user is notified to reload the page to display an updated transition table.
<!-- transactions.php -->
<div class="alert">
<span id="alert_box" class="closebtn" onclick="this.parentElement.style.display='none';">×</span>
⚠️ Reload this window to view the updated transaction list.
</div>
Step 10: Hosting the app
Duration: Approximately 3 days
I struggled to host the PHP app. I’ve never tried hosting an web app that:
- Needs a live server to run.
- Isn’t uploaded on a GitHub repo.
I first tried using AWS Cloud9 and an EC2. All Youtube tutorials I found on PHP hosting on an EC2 instance were outdated and the docs kept leading me through an infinite rabbit hole of pre requisite steps to be accomplished. It was too frustrating since my app is dynamic. Using Cloud9 was less complicated than using an EC2 instance but I found another alternative that worked better for me.
I threw my app into a Docker container and used fly.io to deploy the app after importing my code files.
Challenges encountered
I went through 4 full iterations of my app rectifying my code and UX designs before coming up with the final product. Here are the major challenges I experienced:
-
Learning curve: Being new to PHP, I was initially overwhelmed by it; trying to understand how it will fit into the general structure of the app. Basically where JavaScript started and PHP ended. I used some written PHP courses and a ton of Googling to find my way out of unfamiliar errors.
-
Building the PHP proxy script: Since I was unfamiliar with this, I googled the heck out of it to fully understand its purpose. I got stuck for 2 days trying to figure out why my
GETrequests were responding with some missing information. I used Hoppscotch to narrow down the source of my problem. Fortunately, Hoppscotch allows you to get raw PHP curl code when you make a request. I realized that my URL string was off and I fixed it. -
Building a good user experience, improving maintainability and readability: I thought about how I wanted my designs to look - simple and appealing. I searched for inspiration from the design gods on dribble.com and got a gist of what I wanted.
I also used consistent and definitive naming conventions. I broke down complex logic into smaller functions, used meaningful error messages, commented on my code and kept functions as short as possible,
Conclusion
I had fun overcoming my PHP fears and biases; I always love a good challenge. If I were to further develop this application, my initial priorities would be increasing feedback on the UI, and optimizing the design for mobile devices.