HeaderSIS.jpg

Difference between revisions of "IS480 Team wiki: 2016T1 Stark Final Wiki"

From IS480
Jump to navigation Jump to search
Line 774: Line 774:
 
<b>Darren Tay Kuang Yong</b>
 
<b>Darren Tay Kuang Yong</b>
 
|style="background: white; text-align: left; font-weight: normal"|  
 
|style="background: white; text-align: left; font-weight: normal"|  
 
+
FYP was a challenging experience, learning how to function as a team, and performing my duties as an individual, were strenuous balancing tasks throughout the semester and even before. There were so many hurdles I met, and many that though I tried, I couldn’t conquer. I also learnt how important it was to have an interest in what I did, and the lack thereof usually resulted in me having to climb an uphill battle without time to catch my breath. I now am aware of my limits, and what I can do, but that does not stop me from challenging these boundaries any further, but tailoring my expectations more reasonably is something I’ve always struggled with, and FYP has helped me overcome that. Additionally, I’m so grateful for this bunch of talented, hardworking team-playing sidekicks that I’ve been fortunate to work alongside these 6 months. Though I’m sad that it feels like the phase is coming to an end, I’m confident that we can always be friends for years to come. While one chapter closes, another chapter, possibly even MaiMai, could start.
 
|}
 
|}
 
<!--/Content End-->
 
<!--/Content End-->

Revision as of 12:32, 6 November 2016

Team Stark Logo.png


Team Stark Home.png   HOME

 

Team Stark About Us.png   ABOUT US

 

Team Stark Project Overview.png   PROJECT OVERVIEW

 

Team Stark Project Management.png   PROJECT MANAGEMENT

 

Team Stark Documentation.png   DOCUMENTATION

 




Project Progress Summary

Final Slides: [ Download Here]

MaiMai is deployed for production at
MaiMai is deployed for testing purpose at
Learn more about us at http://www.maitwo.com

Current Iteration:
Total Iterations: 16
Iteration Dates (Current):
Iteration Dates (Final):


Project Highlights


Project Challenges

Project Achievements


Project Management

Project Scope

Planned
Stark Scope midterm wiki.png
Actual
Stark Scope final.png


Scope Changes The following changes has been made to our project scope and reflected in our schedule.

  • Altered "Location Check-In" to "Agreement fields (Location, Date and Time)" as check-In process requires much more planning and considerations. As there is limited time, the team has decided to focus on providing as much structured agreements as possible to show future plan of using such structured agreements as a mean to automate the dispute management process.
  • Added "Web Push Notification" into "Good-To-Have" feature to integrate the user's experience with the notification function in MaiMai.

Project Timeline

Planned
Stark schedule after.png
Actual
Stark schedule final.png



Schedule Highlights The following changes has been made to our project scope and reflected in our schedule.

  • iBanking was shifted from iteration 12 to iteration 13 as we need to replan the business process as there is no way for savings account to be automated
  • Added "Web Push Notification" into "Good-To-Have" feature in iteration 13 as to integrate the user's experience with the notification function in MaiMai
  • Altered "Location Check-In" to "Agreement fields (Location, Date and Time)" as check-In process requires much more planning and considerations. As there is limited time, the team has decided to focus on providing as much structured agreements as possible to show future plan of using such structured agreements as a mean to automate the dispute management process.
  • Shifted UT3 from 24 Oct to 31 Oct as additional features were being modified and added into scope, UT3 will focus on all additional features.

Refer to our Change Management for more!

Project Metrics

Schedule Metrics

[[File:|600px|center|link=]]

Iteration Planned Duration (Days) Actual Duration (Days) Schedule Metric Score Action Taken Status
8 14 16 0.88 More time needed to fix bugs from UT1 and after the integration of Dispute Management.

Follow up action: Informed supervisor and mentor about the delay. More hours put in for debugging. No major delay to overall schedule.

Completed

View our Schedule Metrics Here!

Bug Metrics

[[File:|600px|center|link=]]

Iteration Bug Score Bug Summary Action Taken
6 43

3 low
8 high
0 critical

The spike in bug count is due to the integration of dispute management system together with previous completed features. Resolve bugs immediately. No major delay was caused.
8 179

44 low
21 high
3 critical

The spike in bug count is because of the integration of many new features together for UT1. Resolve bugs immediately. Iteration 8 was scheduled for post UT1 debugging. PM scheduled the Lead Backend developer to focus on debugging while the rest of the developers focus on development.
9 42

7 low
3 high
2 critical

The first critical bug was system crashed when a dispute for review is filed. The second critical bug was images is not updated when listing image name is being edited. Resolve bugs immediately. No major delay was caused. Critical bug did not occur again. Similar action was performed but bug did not replicate.

Viewed our Bug Metrics Here!


Project Risks

Risk Description Likelihood Impact Mitigation Strategy
Mismatch of product against market. High High
Developed product may not be what our target users or anyone in the market wants.
1. Gather more feedbacks during testing to validate market needs against project idea.

2. Transact with more real user and conduct interview with them to understand the problems and needs of the market.

Quick rise of direct competition Low Low
Existing application like Caurosell can easily implement payment services that we are providing.
We provide user with dispute management service which is something that is unique to us and we are investing our resources into defining a stringent and reliable process of micro management. As such we have the upper hand because that is our prime feature. However, it still remain as a risk.

Technical Complexity

Complexities arise due to the following needs:

  • User Experience
  • Performance (Client and Server)
  • Compatibility across devices and browsers
  • Mobile vs Desktop

Processing of Users’ Uploaded Images

Problem: Beforehand we had a “limit” on the size of images to 1MB

  • Server resources
  • Faster loading for users

Causes: Poor user experience

  • Users complain why other “apps” out there that can accept all images from their phones (main basis of benchmarking).


Uploadimage1.png Uploadimage2.png

Benchmark max file size to accept:

  • Two “best” phones (iPhone 7s and Samsung Galaxy 7 camera captured photos (12 MP 3-6 MB)

Keep file sizes after processing below 1MB

  • Maintaining sufficient quality for the web

Processing to be done client side

  • If image processing is to be at server side there is an additional two way transfer of the user uploading to the server and downloading the processed image
  • Heavy on server resources

What we did:

  • Used HTML’s Canvas for resizing
  • Client-side library Pica for reducing image quality

Explaination:
1. Retrieve images from form
2. Create HTML Image object
3. Load uploaded image into Image object

  //Goes through the images ( might have multiple images at one go)
  let images = this.state.image;
  for (let i = 0; i < images.length; i++) {
    let image = images[i];
    let img = new Image();
    img.src = window.URL.createObjectURL(image);
    img.onload = () => {

4. Create a HTML Canvas object
5. Draw Image object onto Canvas

  img.onload = () => {
   let src = document.createElement('canvas');
   src.width = img.width;
   src.height = img.height;
   let ctx = src.getContext('2d');
   ctx.drawImage(img, 0, 0);

6. Determine processed Canvas size while maintaining ratio

   let dest = document.createElement('canvas');
   let longerLength = 1536;
   let shorterLength = 1180;
   let ratio;
   if ((src.width > src.height || src.width === src.height) &&
       (src.width > longerLength || src.height > shorterLength)) {
       if (src.width / longerLength > 1) {
           ratio = longerLength / src.width;
           dest.width = Math.round(ratio * src.width);
           dest.height = Math.round(ratio * src.height);
       } else {
           ratio = src.width / longerLength;
           dest.height = Math.round(ratio * src.height);
           dest.width = Math.round(ratio * src.width);
       }

7. Use Pica to do image optimization from source Canvas to destination Canvas

  pica.resizeCanvas(src, dest, (err) => {
    if (err) {
       console.log(err);
       return;
    }
     dest.toBlob(processImage, 'image/jpeg', 0.8);
  });

8. With processed Canvas ready, we convert it into a blob and then into a Buffer Object to transfer to backend later

  const processImage = (blob) => {
    const data = (file) => {
       let reader = new FileReader();
       let name = file.name;
       reader.onloadend = (e) => {
           let results = this.state.uploadData;
           let sha1 = this.state.sha1;
             
           let image = reader.result;
           let buffer = new Buffer(image.byteLength);
           let view = new Uint8Array(image);
           for (let i = 0; i < buffer.length; ++i) {
               buffer[i] = view[i];
           }

9. We later realized that iOS browsers do not support the Canvas function “toBlob”

  export const addToBlobFunction = function () {
   // add toBlob functionality for browsers that do not support it.
   if( !HTMLCanvasElement.prototype.toBlob ) {
       Object.defineProperty( HTMLCanvasElement.prototype, 'toBlob', {
           value: function( callback, type, quality ) {
               const bin = atob( this.toDataURL( type, quality ).split(',')[1] ),
                   len = bin.length,
                   len32 = len >> 2,
                   a8 = new Uint8Array( len ),
                   a32 = new Uint32Array( a8.buffer, 0, len32 );
    
               for( var i=0, j=0; i < len32; i++ ) {
                   a32[i] = bin.charCodeAt(j++)  |
                       bin.charCodeAt(j++) << 8  |
                       bin.charCodeAt(j++) << 16 |
                       bin.charCodeAt(j++) << 24;
               }
 
               let tailLength = len & 3;
               while( tailLength-- ) {
                   a8[ j ] = bin.charCodeAt(j++);
               }

End Result: Image sizes reduced from (5.5MB to 0.4MB, 8% of original image size)

Image-beforeafter.png

Reactive and Real-time Chat

The chat serves as the core foundation to the transaction process for buyers and sellers Its’ main purpose is to facilitate efficient and effective communication and transaction between users. It needed to be reactive and real-time:

  • Needs to respond to changes by either user especially when building up the initial agreements
  • Needs to reflect proper status and relevant information according to the stage in the transaction

Reactivity and Real-Time achieved with Meteor and React:

  • Meteor provide the real time syncing
  • React allows us to react to changes in data
Real time.png

Designing of the data “schemas” is especially important Required to allow for changes to be properly reflected Caters to a publish-subscribe methodology

 let toInsert = {
   "_id": chatId,
   "listing": listingId,
   "listingOwner": listingOwner,
   "otherUser": otherUser,
   "status": "open",
   "price": listing.price,
   "agreements": agreements,
   "privateImages": {},
   createdAt: new Date()
 };
 
 Chats.insert(toInsert);
 Messages.insert({
   "chat": chatId,
   "owner": username,
   "type": "user",
   "message": message,
   "createdAt": new Date()
 })
 case 'add':
   if (currentAgreement) throw new Meteor.Error('agreement already exists');
   agreements[agreement] = {
       agreement: agreement,
       owner: username,
       status: 'add'
   };

Using Meteor for data subscription, data changes are continuously pushed to the React Component

  export default createContainer(({params}) => {   
     // …
     const chatHandle = Meteor.subscribe('getChat', chatId, {
        // …
     });
     // …
  }, Conversation);
  componentWillReceiveProps(nextProps) {
     let {user, chat, transaction, payment} = nextProps;

Using React’s lifecycle management (componentWillReceiveProps), we respond to changes whenever it receives new data

  componentWillReceiveProps(nextProps) {
     let chat = nextProps.chat;
     let listing = nextProps.listing;
     if (Object.keys(chat).length > 0) {
       this.setDetails(chat);
     } else if (Object.keys(listing).length > 0) {
       this.setListingDetails(listing);
   }}

For example rendering the agreements and the available actions

  agreements.forEach((val) => {
     let agreement = val.agreement;
     let owner = val.owner;
     let status = val.status;
     switch (status) {
        case 'confirmed':
           contentConfirmed.push(confirmedAgreement(agreement));
           break;
        case 'edit':
           contentEdited.push(pendingAgreement(agreement, 'edit', owner === myUser));
           break;
        case 'delete':
           contentEdited.push(pendingAgreement(agreement, 'delete', owner === myUser));
           break;

Or showing new chat messages immediately

  getMessage(messageArr) {
    let user = this.props.myUser;
    let messages = [];
    messageArr.map((message) => {
       let {_id: id, owner, type, message: content, createdAt: date} = message;
       switch (type) {
           case 'user':
               messages.push(
                   <UserMessage key={id} message={content} date={date} owner={owner === user} username={user}/>
               );
               break;
           case 'system':
               messages.push(
                   <SystemMessage key={id} message={content} date={date}

Web Push Notifications

Beforehand we only had “notifications” that only works if the user views the web application.
Poor user experience

  • No way for users to find out they have been contacted.
  • Users say that it does not make sense for them to manually periodically check the application
Stark Notification.png


Considerations
1. Provide “push notifications” that does not require user to open MaiMai up as one of their tabs.

2. Support as many web browsers and devices as possible.

  • As we do not have a native mobile app, we need to rely on whatever protocol that is available for the web.


How we did?
Web Push notifications achieved using Service workers and Push API

1. Service workers

  • Run on a different thread to the web app (non-blocking)

2. Push API

  • “gives web applications the ability to receive messages pushed to them from a server, whether or not the web app is in the foreground, or even currently loaded, on a user agent”


What we did?
Initial server side configuration:
1. Server needs to generate and store a elliptic curve Diffie-Hellman (ECDH) public and private key pair.

  "vapidKeys": {  
     "publicKey": "BMPuJKF-1EwQ….",  
     "privateKey": “…"
  },


2. Needs to have the service worker script ready

  • This script serves as the instructions to the client on how to react to different situations.

3. Get user’s permission and install service worker

  navigator.serviceWorker.register('/sw.js').then(() => {
      return navigator.serviceWorker.ready;
  }).then((serviceWorkerRegistration) => {
      BrowserNotification.reg = serviceWorkerRegistration;
      serviceWorkerRegistration.pushManager.getSubscription()
          .then((subscription) => {
              if (!subscription) {
                  BrowserNotification.subscribe(); 
  …

4. Get public key, create subscription and pass subscription details to server for storing

Meteor.call('getPushPublicKey', (err, res) => {
   if (err) return;
   reg.pushManager.subscribe({
       userVisibleOnly: true,
       applicationServerKey: BrowserNotification.urlBase64ToUint8Array(res)
   }).then((pushSubscription) => {
       BrowserNotification.sub = pushSubscription;
       BrowserNotification.isSubscribed = true;
       Meteor.call('setUserSubscription', pushSubscription.toJSON());
       cb();
   });
}) 

5. Subscription details contain the following:
1. Message server’s end point
2. Keys

  • auth – authentication secret generated by the browser
  • p256dh – client’s browser public key for encryption
{
   "endpoint": "https://fcm.googleapis.com/fcm/send/fkLOH68QwQ4:APA9...",
   "keys": {
       "p256dh": "BLX5h_qHwDgrdqp-GGlua-aWyiIRzXUYNdUg84hsOy74Lag-EU…",
       "auth": "uv6nwQTejj2lAA9hPj… “ 
   }
}

6. Implemented server side hooks:

  • New chat messages
  • New notifications

Hooks call “newMessage” from our own web push module (WebPush)

7. To create a push message, we need to generate the following: 1. Headers

  • Authorization (JSON Web Token)
    • Header
    • Payload
    • Signature (Encrypted header + payload)
  • Crypto-Key
    • Public key of server
    • Public key of client’s browser
  • Encryption (random 16 byte salt)

2. Body

  • Encrypted message


8. JSON Web Token generated using JWS package

const header = {
   typ: 'JWT',
   alg: 'ES256'
};

const jwtPayload = {
   aud: audience, 
   exp: Math.floor(Date.now() / 1000) + 86400,
   sub: subject
};

const jwt = jws.sign({
   header: header,
   payload: jwtPayload,
   privateKey: privateKey // the server's private key
}); // this returns us the entire JSON Web Token
Stark Key.png

Crypto-Key is simply a concatenation of:
1. Public key of the server

  • p256ecdsa=<sever’s public key>

2. Public key of the client’s browser

  • dh=<subscription’s public key>
dh=<subscription’s public key>;p256ecdsa=<server’s public key>


9. The message contains the following:

  • Title: of the push notification
  • Body: of the push notification
  • Redirect: link to go to upon clicking
  • Tag: for combining of messages
let options = {
   title: username,
   body: message, 
   redirect: "/chat/" + chatId,
   tag: chatId
};


10. To encrypt the message, we used the library http_ece

   const localCurve = crypto.createECDH('prime256v1');
   const localPublicKey = localCurve.generateKeys();

   const salt = urlBase64.encode(crypto.randomBytes(16));

   ece.saveKey('webpushKey', localCurve, 'P-256');

   const cipherText = ece.encrypt(payload, {
       keyid: 'webpushKey',
       dh: userPublicKey,
       salt: salt,
       authSecret: userAuth, 
       padSize: 2
   });

   return {
       localPublicKey: localPublicKey,
       salt: salt,
       cipherText: cipherText
   };

11. We send a POST request to the subscription end point with the encrypted message and headers:

while (!successful && new Date() - timeout < 60000) {
   //assumes that after a min or ~60 tries and still 
unable to send and receive the subscription is down
   try {
       res = HTTP.call(requestDetails.method, requestDetails.endpoint, options);
       successful = true;
       addToUpdate = true;
   } catch (err) {
       if (err.response && err.response.statusCode == '410') {
           successful = true; 
           unsuccessful.push(sub.endpoint);
       }
       setTimeout(()=>{}, 1000);
   }

12. Users will then receive the push notification on their browsers

  • Desktop browsers must be opened to receive the notifications
  • Android’s Chrome will receive in the background regardless of being opened or closed

On clicking the notification, it will go to the relevant window and redirect to the link.

Stark Notifi.png

13. The way to react to push notifications are scripted within the service worker script we installed earlier.

We specify 2 main events

  • push: When receiving a push notification
  • notificationclick: When clicking on a push notification

Push event has 3 main steps
1. Get all notifications that are within the same “tag”, this allows us to combine messages. Combine if more than one message.

const promiseChain = self.registration.getNotifications({tag: event.data.tag})
   .then(notifications => {
       …
           if (notifications.length > 0) { //we need to combine
               options.body = "You have received new messages";
               options.renotify = true;

2. Check if user is still viewing the app, if so do not show notification

return clients.matchAll()
   .then(clients => {
       let mustShowNotification = true;
       if (clients.length > 0) {
           for (let i = 0; i < clients.length; i++) {
               if (clients[i].visibilityState === 'visible') {
                   mustShowNotification = false;

3. Show the notification

if (mustShowNotification) {
   // Show the notification.
   if (options) {
       return self.registration.showNotification(
           options.title ? options.title : "MaiMai", options);

notificationclick event redirects user to the page of the notification

event.waitUntil(clients.matchAll({
       includeUncontrolled: true,
       type: 'window'
   }).then(activeClients => {
       if (activeClients.length > 0) {
           if ("navigate" in activeClients[0]) {
               activeClients[0].navigate(appUrl);
               activeClients[0].focus();
           } else { //workaround for firefox

clients.openWindow(appUrl);

           }
       } else {
           clients.openWindow(appUrl);
       }
   })
);

Final outcome
Users can now receive notifications without opening the web application.

Supported clients: 1. Desktop (Windows/Mac OSX):

  • Chrome
  • Firefox

2. Mobile (Android):

  • Chrome (or any variants that is based of chrome)
  • Firefox


Quality of Product

Intermediate Deliverables

Stage Specification Modules
Project Requirements Market Research Market Research
Project Management Minutes Minutes
Metrics Schedule Metrics
Bug Metrics
Risk Risks
Change Management Change Management
Diagrams Use Diagrams
Architecture Diagram
Diagrams
Design Low-Fi Prototype
High-Fi Prototype
UI Prototype
Testing User Testing 1 User Test 1
User Test 2
User Test 3

Deployment


User Testing

User Testing Date Venue Users Link
User Testing 1 1st August 2016 - 8th August 2016 Off-Site (At respective members' discretion) 22 User Testing 1
User Testing 2 12th September 2016 - 26th September 2016 SIS GSR 2-7 & Off-Site (At respective members' discretion) 38 User Testing 2
User Testing 3 User Testing 3


Reflection

Team Reflection The journey so far has been tough but enjoyable learning experience for Team Stark. As a self-proposed IS480 project, we learnt how to make use of this opportunity to learn about running a start-up. For example, the importance of gathering feedbacks from actual users, validating ideas and features, followed by developing an application that caters to market needs. We have learnt about individual's strengths and weaknesses and worked towards complementing each other to produce quality work. Furthermore, given the guidance of our supervisor, mentor, and reviewers helps the team to overcome obstacles and stretch us to our maximum capability.


Individual Reflection

Name Reflections
Stark Ivy.png

Lee Chian Yee

FYP has been a tough but meaningful journey. It was challenging to plan for a good schedule when changes in project scope occur. There were many things to take into considerations before making responsible changes in schedule that best fit everyone in the team. As every members has other modules to handle, I am grateful to have such co-operative and considerate team members who put in additional effort in meeting deadlines. On top of this, I am also thankful for having great supervisor and mentor who guided us with useful advice and take time out from their busy schedule to meet us at our convenient time-slot and venue. One of the most valuable learning I achieve during this journey is that feedback received from user testing is really important. They are able provide us with real user comments that we might have overlooked and thus allow us to improve from. However, the overall most important learning that I have gained is to really enjoy this entire journey, appreciate every team members and work towards our goals together.

Stark Hem.png

Raji Hemanth Kumar

Stark Qx.png

Lee Qixian

FYP’s a marathon, not a sprint. It has been a long, tiring but ultimately rewarding journey. I have learnt many different things. Within the group, I learn about managing group and individual expectations, understanding the limits of people and more importantly trusting that people will do their best especially if everyone is targeting the same goal. In terms of my personal technical skills, I have explored further into the fascinating world of JavaScript and web based technologies, understanding the huge flaws of the language and the troubles of developing for the web. It is great to finally be able to apply the concepts and theories I learnt throughout my IS journey. It feels fantastic too to see real users actually trying out the application and providing feedback. “This is not the end, this is not even the beginning of the end, this is just perhaps the end of the beginning.” – Winston S. Churchill

STARK Josh.png

Phua Xue Yong

Stark Qp.png

Lin Qianpin

Stark Darren.png

Darren Tay Kuang Yong

FYP was a challenging experience, learning how to function as a team, and performing my duties as an individual, were strenuous balancing tasks throughout the semester and even before. There were so many hurdles I met, and many that though I tried, I couldn’t conquer. I also learnt how important it was to have an interest in what I did, and the lack thereof usually resulted in me having to climb an uphill battle without time to catch my breath. I now am aware of my limits, and what I can do, but that does not stop me from challenging these boundaries any further, but tailoring my expectations more reasonably is something I’ve always struggled with, and FYP has helped me overcome that. Additionally, I’m so grateful for this bunch of talented, hardworking team-playing sidekicks that I’ve been fortunate to work alongside these 6 months. Though I’m sad that it feels like the phase is coming to an end, I’m confident that we can always be friends for years to come. While one chapter closes, another chapter, possibly even MaiMai, could start.