Scott Mendenhall
Published © CC BY-NC

Dash Replenishment iOS App

A quick way to use dash replenishment services in an easy-to-use iOS App.

IntermediateFull instructions provided1 hour891
Dash Replenishment iOS App

Things used in this project

Hardware components

iPhone
Apple iPhone
Or any other iOS device.
×1

Software apps and online services

Xcode
Apple Xcode
You also need to sign in to Xcode with your Apple Developer Account.

Story

Read more

Code

AppDelegate.swift

Swift
This is the AppDelegate file, which is required to make the app run.
//
//  AppDelegate.swift
//  DRS
//
//  Created by Mendenhall on 12/20/16.
//  Copyright  2016 Mendenhall. All rights reserved.
//

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }


}

Main.swift

Swift
This is the main swift code that the app runs.
//
//  MainController.swift
//  DRS
//
//  Created by Mendenhall on 12/20/16.
//  Copyright  2016 Mendenhall. All rights reserved.
//

import UIKit
import Alamofire

// Uncomment lines you would like to use
// If you use the commented value for device_model you MUST use only the slots in lines 25 - 40

let client_id = "YOUR_CLIENT_ID_HERE" //"amzn1.application-oa2-client.2be6672047d7412c8d5fb878d9c2cf48"
let client_secret = "YOUR_CLIENT_SECRET_HERE" //"5564a2ae7500e0674057325eb0ca916fb0998c8984aee83c1426bbe283489970"
let device_model = "YOUR_DEVICE_ID_HERE" //"6d8e5069-2150-45c1-8d33-60a1617b0b03"
let serial = "YOUR_DEVICE_SERIAL_HERE" //"abcd-abcd-abcd-abcd" // This can be anything just make something unique

let slots: [Slot] = [

    //     Your slots here with this format
    //     Slot(name: "SLOT_NAME", id: "SLOT_ID"),
    
         Slot(name: "Batteries", id: "4cd07e7d-78fa-4916-9442-1b2476c8bd5a"),
         Slot(name: "Cat Food", id: "a743fc5f-dceb-4d43-95e9-3190dc6e816d"),
         Slot(name: "Deodorant", id: "10490a67-5d84-43fd-b040-81642455d290"),
         Slot(name: "Dog Food", id: "f148029b-33ee-4cbf-a949-cbf78716b12d"),
         Slot(name: "Jojoba Oil", id: "f68e8606-4539-4461-bf96-c56660af0aca"),
         Slot(name: "Keurig K-Cups", id: "472be77f-8465-4615-85d4-625dad35e2ea"),
         Slot(name: "Lemonade", id: "f271c470-d3de-4f85-8aae-7eb128118546"),
         Slot(name: "Medicine", id: "0345968b-586e-4d57-a105-e312b6bf4859"),
         Slot(name: "Paper Towels", id: "ab57ed15-a41e-4ee8-b935-dba5e234cbbe"),
         Slot(name: "Pencils", id: "0ad28ac1-9e92-4c15-ba2a-a5a30a920c7d"),
         Slot(name: "Popcorn", id: "a2181e64-aafb-4c45-a1b2-418ae34e4420"),
         Slot(name: "Printer Paper", id: "fbabb6d2-6c74-47a9-b8ab-b6fde8c0dbd6"),
         Slot(name: "Protein Bars", id: "5935d1f2-cd9a-4207-acda-f350fdc4f183"),
         Slot(name: "Toilet Paper", id: "58aaeab3-366e-441a-9121-4fa7c55b206a"),
         Slot(name: "Trash Bags", id: "adb1cc1a-6547-42c5-8b16-cc679c5623ea"),
         
         Slot(name: "Water Bottles", id: "99f2ffce-982f-40a9-ad41-f366e3eca95b")

]

let theme_color = UIColor(red: 1, green: 0.6, blue: 0, alpha: 1)

class MainController: UIViewController, SlotViewDelegate {

    @IBOutlet var navigation: UINavigationBar!
    @IBOutlet var action_item: UIBarButtonItem!
    @IBOutlet var helpitem: UIBarButtonItem!
    @IBOutlet var scroll: UIScrollView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let spacer = CALayer()
        spacer.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 20)
        spacer.backgroundColor = navigation.barTintColor?.cgColor
        self.view.layer.insertSublayer(spacer, at: 0)
        
        let slot_space: CGFloat = 20
        let slot_height: CGFloat = 116
        let slot_width = self.view.frame.width - slot_space * 2
        
        self.scroll.contentSize = CGSize(width: self.view.frame.width, height: 20 + CGFloat(slots.count) * (slot_height + slot_space))
        
        for i in 0 ..< slots.count {
            let slot = slots[i]
            let slotview = Bundle.main.loadNibNamed("SlotView", owner: self, options: nil)?.first as! SlotView
            slotview.frame = CGRect(x: slot_space, y: slot_space + CGFloat(i) * (slot_height + slot_space), width: slot_width, height: slot_height)
            slotview.name.text = slot.name
            slotview.number.text = "Slot #\(i + 1)"
            slotview.index = i
            slotview.delegate = self
            self.scroll.addSubview(slotview)
        }
        
        let defs = UserDefaults.standard
        if let acquired_time = defs.value(forKey: "token_time") {
            let now = Date().timeIntervalSinceReferenceDate
            let passed = now - (acquired_time as! TimeInterval)
            print("Time passed since acquisition of tokens: \(passed)")
        }
    }
    
    func selected(view: SlotView) {
        let slot = slots[view.index]
        let alert = UIAlertController(title: "\(slot.name)", message: "Do you want to refill this slot? An order will be placed for the item you have selected for this slot.", preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "Refill", style: .default, handler: { (action) in
            let defs = UserDefaults.standard
            if let acquired_time = defs.value(forKey: "token_time") {
                let now = Date().timeIntervalSinceReferenceDate
                let passed = now - (acquired_time as! TimeInterval)
                print("Time passed since acquisition of tokens: \(passed)")
                if passed > 3600 {
                    print("Tokens are expired and need to be renewed!")
                    let refresh_token = defs.value(forKey: "refresh_token") as! String
                    Alamofire.request("https://api.amazon.com/auth/o2/token", method: .post, parameters: [
                        "grant_type": "refresh_token",
                        "refresh_token": refresh_token,
                        "client_id": client_id,
                        "client_secret": client_secret,
                        ], encoding: URLEncoding.default, headers: [
                            "Content-Type": "application/x-www-form-urlencoded"
                        ]).responseJSON(completionHandler: { (response) in
                            let json = response.result.value as! [String: Any]
                            print(json)
                            let access_token = json["access_token"] as! String
                            let refresh_token = json["refresh_token"] as! String
                            
                            let defs = UserDefaults.standard
                            defs.set(access_token, forKey: "access_token")
                            defs.set(refresh_token, forKey: "refresh_token")
                            defs.set(Date().timeIntervalSinceReferenceDate, forKey: "token_time")
                            defs.synchronize()
                            
                            print("Saved new tokens...")
                            self.refill(slot)
                        })
                } else {
                    print("Tokens are valid!")
                    self.refill(slot)
                }
            } else {
                print("No tokens have been saved.")
            }
        }))
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        self.present(alert, animated: true, completion: nil)
        alert.view.tintColor = theme_color
    }
    
    @IBAction func help(_ sender: Any) {
        site_state = .Help
        self.performSegue(withIdentifier: "site", sender: self)
    }
    
    @IBAction func login(_ sender: Any) {
        let alert = UIAlertController(title: "Actions", message: "You may register this app as a device linked to your Amazon account or change this device's settings.", preferredStyle: .actionSheet)
        alert.addAction(UIAlertAction(title: "Login or Register", style: .default, handler: { (action) in
            site_state = .Login
            self.performSegue(withIdentifier: "site", sender: self)
        }))
        alert.addAction(UIAlertAction(title: "Manage Device Settings", style: .default, handler: { (action) in
            UIApplication.shared.openURL(URL(string: "https://www.amazon.com/mn/dcw/myx.html#/home/devices/1")!)
        }))
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
        let over = alert.popoverPresentationController
        over?.barButtonItem = action_item
        over?.permittedArrowDirections = .up
        self.present(alert, animated: true, completion: nil)
        alert.view.tintColor = theme_color
    }
    
    func refill(_ slot: Slot) {
        print("Refilling slot: \(slot)")
        let access_token = UserDefaults.standard.value(forKey: "access_token") as! String
        Alamofire.request("https://dash-replenishment-service-na.amazon.com/replenish/\(slot.id)", method: .post, parameters: nil, encoding: URLEncoding.default, headers: [
            "Authorization": "Bearer \(access_token)",
            "x-amzn-accept-type": "com.amazon.dash.replenishment.DrsReplenishResult@1.0",
            "x-amzn-type-version": "com.amazon.dash.replenishment.DrsReplenishInput@1.0"
        ]).responseJSON(completionHandler: { (response) in
            let json = response.result.value as! [String: Any]
            if let errmsg = json["message"] {
                let smsg = errmsg as! String
                let alert = UIAlertController(title: "Error!", message: smsg, preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
                self.present(alert, animated: true, completion: nil)
                alert.view.tintColor = theme_color
            }
            if let code = json["detailCode"] {
                let scode = code as! String
                var title = ""
                var message = ""
                if scode == "ORDER_INPROGRESS" {
                    title = "Alert"
                    message = "No order was placed because an order is already in progress. There is an order still out for delivery to you."
                } else if scode == "TEST_ORDER_PLACED" {
                    title = "Success (TEST)"
                    message = "An order was successfully placed with the test flag. All notifications will proceed normally but you will not be charged and the item will not ship."
                } else if scode == "STANDARD_ORDER_PLACED" {
                    title = "Success"
                    message = "An order was successfully placed and will be shipped to you."
                }
                let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
                alert.addAction(UIAlertAction(title: "Ok", style: .default, handler: nil))
                self.present(alert, animated: true, completion: nil)
                alert.view.tintColor = theme_color
                print(json)
            }
        })
    }
    
}

struct Slot {
    
    var name: String
    var id: String
    
}

class NavigationBar: UINavigationBar {
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        let ctx = UIGraphicsGetCurrentContext()
        ctx?.setStrokeColor(UIColor(white: 0.67, alpha: 1).cgColor)
        ctx?.move(to: CGPoint(x: 0, y: self.frame.height))
        ctx?.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
        ctx?.strokePath()
    }
    
}

extension String {
    
    func encodeURIComponent() -> String {
        let set = NSMutableCharacterSet.alphanumeric()
        set.addCharacters(in: "-_.!~*'()")
        return (self.addingPercentEncoding(withAllowedCharacters: set as CharacterSet))!
    }
    
}

Site.swift

Swift
This is the support code for the app to view websites, which are included in the app.
//
//  Site.swift
//  DRS
//
//  Created by Mendenhall on 12/21/16.
//  Copyright  2016 Mendenhall. All rights reserved.
//

import UIKit
import Alamofire

enum SiteState {
    case Login, Help
}

var site_state: SiteState = .Login

var login_url = "https://www.amazon.com/ap/oa" + ("?" + "client_id=" + client_id + "&scope=dash:replenish" + "&scope_data=" + "{\"dash:replenish\":{\"device_model\":\"\(device_model)\",\"serial\":\"\(serial)\",\"is_test_device\":true}}" + "&response_type=code" + "&redirect_uri=https://www.google.com").encodeURIComponent()

let login_url_req = URLRequest(url: URL(string: login_url)!)
let help_url_req = URLRequest(url: URL(fileURLWithPath: Bundle.main.path(forResource: "help", ofType: "html")!))

class SiteController: UIViewController, UIWebViewDelegate {
    
    @IBOutlet var navigation: NavigationBar!
    @IBOutlet var webview: UIWebView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        switch site_state {
        case .Login:
            navigation.topItem?.title = "Login"
            webview.delegate = self
            webview.loadRequest(login_url_req)
            break
        case .Help:
            navigation.topItem?.title = "Help"
            webview.loadRequest(help_url_req)
            break
        }
    }
    
    @IBAction func cancel(_ sender: Any) {
        webview.stopLoading()
        self.performSegue(withIdentifier: "close", sender: self)
    }
    
    func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        let url = request.url?.absoluteString
        var parts = url?.components(separatedBy: "?")
        if parts?[0] == "https://www.google.com/" {
            parts = parts?[1].components(separatedBy: "&")
            let login_code = (parts?[0].substring(from: (parts?[0].index((parts?[0].startIndex)!, offsetBy: 5))!))!
            let headers: HTTPHeaders = [
                "Content-Type": "application/x-www-form-urlencoded",
                "Cache-Control": "no-cache"
            ]
            let params: Parameters = [
                "grant_type": "authorization_code",
                "code": login_code,
                "client_id": client_id,
                "client_secret": client_secret,
                "redirect_uri": "https://www.google.com"
            ]
            print("Requesting tokens...")
            Alamofire.request("https://api.amazon.com/auth/o2/token", method: .post, parameters: params, encoding: URLEncoding.httpBody, headers: headers).responseJSON(completionHandler: { (response) in
                let json = response.result.value as! [String: Any]
                let access_token = json["access_token"] as! String
                let refresh_token = json["refresh_token"] as! String
                
                let defs = UserDefaults.standard
                defs.set(access_token, forKey: "access_token")
                defs.set(refresh_token, forKey: "refresh_token")
                defs.set(Date().timeIntervalSinceReferenceDate, forKey: "token_time")
                defs.synchronize()
                
                print("Saved tokens...")
                self.performSegue(withIdentifier: "close", sender: self)
            })
            return false
        }
        return true
    }
    
}

class HideSegue: UIStoryboardSegue {
    
    override func perform() {
        let srcv = self.source.view!
        let dstv = self.destination.view!
        
        let window = UIApplication.shared.keyWindow!
        let size = window.frame.size
        
        srcv.removeFromSuperview()
        window.addSubview(dstv)
        window.addSubview(srcv)
        
        srcv.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
        dstv.frame = srcv.frame
        
        UIView.animate(withDuration: 0.4, delay: 0.0, options: .curveEaseOut, animations: { () -> Void in
            srcv.frame = CGRect(x: 0, y: size.height, width: size.width, height: size.height)
        }, completion: { (complete: Bool) -> Void in
            self.source.present(self.destination, animated: false, completion: nil)
        })
    }
    
}

SlotView.swift

Swift
This adds the custom look of the slots in the app.
//
//  SlotView.swift
//  DRS
//
//  Created by Mendenhall on 12/22/16.
//  Copyright  2016 Mendenhall. All rights reserved.
//

import UIKit

protocol SlotViewDelegate {
    
    func selected(view: SlotView)
    
}

class SlotView: UIView {

    @IBOutlet var name: UILabel!
    @IBOutlet var number: UILabel!
    
    var index: Int = 0
    var delegate: SlotViewDelegate!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        self.layer.shadowRadius = 10
        self.layer.shadowOffset = CGSize(width: 0, height: 0)
        self.layer.shadowColor = UIColor(white: 0.75, alpha: 1).cgColor
        self.layer.shadowOpacity = 0.25
        self.layer.shouldRasterize = true
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        let ctx = UIGraphicsGetCurrentContext()
        ctx?.setStrokeColor(UIColor(white: 0.67, alpha: 1).cgColor)
        ctx?.addRect(self.bounds)
        ctx?.strokePath()
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        if delegate != nil { delegate.selected(view: self) }
    }
    
}

DRS.zip

Swift
This is the whole Xcode project in a zip file.
No preview (download only).

Credits

Scott Mendenhall

Scott Mendenhall

3 projects • 8 followers
Mechanical Engineering Student at UNC Charlotte

Comments