hs-pkpass (empty) → 0.1.0.0
raw patch · 5 files changed
+534/−0 lines, 5 filesdep +aesondep +basedep +conduitsetup-changed
Dependencies added: aeson, base, conduit, directory, filesystem-conduit, old-locale, random, shakespeare-text, shelly, text, time, uuid
Files
- LICENSE +30/−0
- Passbook.hs +122/−0
- Passbook/Types.hs +338/−0
- Setup.hs +2/−0
- hs-pkpass.cabal +42/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2012, Vincent Ambo++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Vincent Ambo nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Passbook.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE ExtendedDefaultRules #-}+{-# LANGUAGE OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-type-defaults #-}+++{- |This module provides different functions to sign a 'Pass' using Apple's @signpass@+ command-line tool.++ I intend to move the signing process to a Haskell native OpenSSL binding later+ on, but due to time constraints didn't get around to it yet.++ The function 'signpass' creates a random UUID during the signing process, uses this UUID+ as the passes' serial number and returns it along with the path to the signed pass.++ The funciton 'signpassWithModifier' creates a random UUID during the signing process,+ passes this UUID on to a function that modifies the pass accordingly (e.g. sets the serial+ number or the barcode payload to the UUID) and otherwise works like 'signpass'.++ The function, 'signpassWithId', takes an existing ID and signs the pass+ using that for the serial number and file name. This will most likely be used+ for updating existing passes.++ Using the function is very simple, assuming you have created a 'Pass' called+ @myPass@ and you have the related assets (e.g. the logo.png and icon.png files)+ stored in a folder named @myPass/@.++ You want the signed pass to be stored in a folder called @passes/@. You call+ 'signpass' like this:++ > (path, passId) <- signpass "myPass" "passes" myPass++ You will find the pass at @path@ with the filename @passId.pkpass@. Using the+ types from "Passbook.Types" ensures that passes are generated correctly.++ The @signpass@ utility must have access to your Keychain to be able to retrieve+ your Pass Type Certificate for the pass to sign. You should run it once outside+ of any program to grant it full access to your Keychain.++ Please note that an @icon.png@ file /must be/ present in your asset folder,+ otherwise the generated pass will not work.++ Refer to Apple's Passbook documentation at <https://developer.apple.com/passbook/>+ for more information or to retrieve the @signpass@ tool which is included in the+ Passbook Support Materials. (iOS Developer Membership necessary)++ This module will most likely only work on OS X machines.+-}+module Passbook (signpass, signpassWithId, genPassId) where++import Data.Aeson+import Data.Conduit+import Data.Conduit.Binary hiding (sinkFile)+import Data.Conduit.Filesystem+import qualified Data.Text as ST+import Data.Text.Lazy as LT+import Data.UUID+import Passbook.Types+import Prelude hiding (FilePath)+import Shelly+import System.Directory (doesFileExist)+import System.Random+default (LT.Text)++-- |Takes the filepaths to the folder containing the path assets+-- and the output folder, a 'Pass' and uses a random UUID to+-- create and sign the pass.+signpass :: FilePath -- ^ Input file path (asset directory)+ -> FilePath -- ^ Output file path+ -> Pass -- ^ The pass to sign+ -> IO (FilePath, ST.Text) -- ^ The filepath of the signed .pkpass and its UUID+signpass passIn passOut pass = do+ passId <- genPassId+ passPath <- signpassWithId passId passIn passOut pass+ return (passPath, passId)++-- |Works like 'signpass', except for the fourth argument which is a+-- modifier function that updates the pass with the generated UUID.+-- This is useful for cases where you want to store the UUID in the barcode+-- or some other field on the pass as well.+signpassWithModifier :: FilePath -- ^ Input file path (asset directory)+ -> FilePath -- ^ Output file path+ -> Pass -- ^ The pass to sign+ -> (ST.Text -> Pass -> Pass) -- ^ Modifier function+ -> IO (FilePath, ST.Text) -- ^ The filepath of the signed .pkpass and its UUID+signpassWithModifier passIn passOut pass modifier = do+ passId <- genPassId+ passPath <- signpassWithId passId passIn passOut $ modifier passId pass+ return (passPath, passId)++-- |Creates and signs a 'Pass' with an existing ID.+signpassWithId :: ST.Text -- ^ The pass ID+ -> FilePath -- ^ Input file path (asset directory)+ -> FilePath -- ^ Output file path+ -> Pass -- ^ The pass to sign+ -> IO FilePath+signpassWithId passId passIn passOut pass = shelly $ do+ let tmp = passOut </> passId+ lazyId = fromStrict passId+ cp_r passIn tmp+ liftIO $ renderPass (tmp </> "pass.json") pass { serialNumber = passId }+ signcmd lazyId tmp passOut+ rm_rf tmp+ return (passOut </> (LT.append lazyId ".pkpass"))++-- |Generates a random UUID for a Pass using "Data.UUID" and "System.Random"+genPassId :: IO ST.Text+genPassId = randomIO >>= return . ST.pack . toString++-- |Render and store a pass.json at the desired location.+renderPass :: FilePath -> Pass -> IO ()+renderPass path pass =+ let rendered = sourceLbs $ encode pass+ in runResourceT $ rendered $$ sinkFile path++-- |Call the signpass tool.+signcmd :: Text -- ^ The pass identifier / serial number to uniquely identify the pass+ -> FilePath -- ^ The temporary asset folder.+ -> FilePath -- ^ The output folder for all .pkpass files+ -> Sh ()+signcmd uuid assetFolder passOut =+ run_ "signpass" [ "-p", toTextIgnore assetFolder -- The input folder+ , "-o", toTextIgnore $passOut </> (LT.append uuid ".pkpass") ] -- Name of the output file
+ Passbook/Types.hs view
@@ -0,0 +1,338 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++{-# OPTIONS_HADDOCK -ignore-exports #-}+++{- |This module provides types and functions for type-safe generation of PassBook's @pass.json@ files.++ This is a complete implementation of the Passbook Package Format Reference, available at+ <https://developer.apple.com/library/ios/#documentation/UserExperience/Reference/PassKit_Bundle/Chapters/Introduction.html>.+++ It ensures that passes are created correctly wherever possible. Currently, NSBundle localization is not supported.+-}+module Passbook.Types where++import Data.Aeson+import Data.Aeson.TH+import Data.Aeson.Types+import Data.Text (Text, pack)+import Data.Time+import System.Locale+import Text.Shakespeare.Text++-- | Auxiliary class to ensure that field values are rendered correctly+class ToJSON a => ToPassField a++instance ToPassField Int+instance ToPassField Double+instance ToPassField PassDate+instance ToPassField Text --where++-- * Passbook data types++type Encoding = Text+type Message = Text++-- |A location field+data Location = Location {+ latitude :: Double -- ^ Latitude, in degrees, of the location (required)+ , longitude :: Double -- ^ Longitude, in degrees, of the location (required)+ , altitude :: Maybe Double -- ^ Altitude, in meters, of the location (optional)+ , relevantText :: Maybe Text -- ^ Text displayed on the lock screen when the pass is relevant (optional)+}++-- |A simple RGB color value. In combination with the 'rgb' function this can be written just like in+-- CSS, e.g. @rgb(43, 53, 65)@. The 'rgb' function also ensures that the provided values are valid.+data RGBColor = RGB Int Int Int++-- |Barcode is constructed by a Barcode format, an encoding+-- type and the Barcode message.+data BarcodeFormat = QRCode+ | PDF417+ | Aztec++-- |A pass barcode. In most cases the helper function 'mkBarcode' should be sufficient.+data Barcode = Barcode {+ altText :: Maybe Text -- ^ Text displayed near the barcode (optional)+ , format :: BarcodeFormat -- ^ Barcode format (required)+ , message :: Text -- ^ Message / payload to be displayed as a barcode (required)+ , messageEncoding :: Text -- ^ Barcode encoding. Default in the mkBarcode functions is iso-8859-1 (required)+}++-- |Pass field alignment+data Alignment = LeftAlign+ | Center+ | RightAlign+ | Natural++-- |Pass field date/time display style+data DateTimeStyle = None -- ^ Corresponds to @NSDateFormatterNoStyle@+ | Short -- ^ Corresponds to @NSDateFormatterShortStyle@+ | Medium -- ^ Corresponds to @NSDateFormatterMediumStyle@+ | Long -- ^ Corresponds to @NSDateFormatterLongStyle@+ | Full -- ^ Corresponds to @NSDateFormatterFullStyle@++-- |Pass field number display style+data NumberStyle = Decimal+ | Percent+ | Scientific+ | SpellOut++-- |A single pass field. The 'value' of a 'PassField' can be anything that is an instance of 'ToPassField'.+-- Dates and numbers are always correctly formatted. If you add another type to 'ToPassField' please make sure+-- that it's corresponding 'ToJSON' instance generates Passbook-compatible JSON.+-- To create a very simple key/value field containing text you can use the 'mkSimpleField' function.+data PassField = forall a . ToPassField a => PassField {+ -- standard field keys+ changeMessage :: Maybe Text -- ^ Message displayed when the pass is updated. May contain the @%\@@ placeholder for the value. (optional)+ , key :: Text -- ^ Must be a unique key within the scope of the pass (e.g. \"departure-gate\") (required)+ , label :: Maybe Text -- ^ Label text for the field. (optional)+ , textAlignment :: Maybe Alignment -- ^ Alignment for the field's contents. Not allowed for primary fields. (optional)+ , value :: a -- ^ Value of the field. Must be a string, ISO 8601 date or a number. (required)++ -- Date style keys (all optional). If any key is present, the field will be treated as a date.+ , dateStyle :: Maybe DateTimeStyle -- ^ Style of date to display (optional)+ , timeStyle :: Maybe DateTimeStyle -- ^ Style of time to display (optional)+ , isRelative :: Maybe Bool -- ^ Is the date/time displayed relative to the current time or absolute? Default: @False@ (optional)++ -- Number style keys (all optional). Not allowed if the field is not a number.+ , currencyCode :: Maybe Text -- ^ ISO 4217 currency code for the field's value (optional)+ , numberStyle :: Maybe NumberStyle -- ^ Style of number to display. See @NSNumberFormatterStyle@ docs for more information. (optional)+}++-- |BoardingPass transit type. Only necessary for Boarding Passes.+data TransitType = Air+ | Boat+ | Bus+ | Train+ | GenericTransit++-- |Newtype wrapper around 'UTCTime' with a 'ToJSON' instance that ensures Passbook-compatible+-- time rendering. (ISO 8601)+newtype PassDate = PassDate UTCTime++-- |The type of a pass including the specific auxiliary, main, etc. fields+data PassType = BoardingPass TransitType PassContent+ | Coupon PassContent+ | Event PassContent+ | GenericPass PassContent+ | StoreCard PassContent++-- |The fields within a pass+data PassContent = PassContent {+ headerFields :: [PassField] -- ^ Fields to be displayed on the front of the pass. Always shown in the stack.+ , primaryFields :: [PassField] -- ^ Fields to be displayed prominently on the front of the pass.+ , secondaryFields :: [PassField] -- ^ Fields to be displayed on the front of the pass.+ , auxiliaryFields :: [PassField] -- ^ Additional fields to be displayed on the front of the pass.+ , backFields :: [PassField] -- ^ Fields to be on the back of the pass.+}++-- |A complete pass+data Pass = Pass {+ -- Required keys+ description :: Text -- ^ Brief description of the pass (required)+ , formatVersion :: Int -- ^ Version of the file format. The value must be 1. (required)+ , organizationName :: Text -- ^ Display name of the organization that signed the pass (required)+ , passTypeIdentifier :: Text -- ^ Pass type identifier, as issued by Apple (required)+ , serialNumber :: Text -- ^ Unique serial number for the pass (required)+ , teamIdentifier :: Text -- ^ Team identifier for the organization (required)++ -- associated app keys+ , associatedStoreIdentifiers :: [Text] -- ^ A list of iTunes Store item identifiers for associated apps (optional)++ -- relevance keys+ , locations :: [Location] -- ^ Locations where the pass is relevant (e.g. that of a store) (optional)+ , relevantDate :: Maybe PassDate -- ^ ISO 8601 formatted date for when the pass becomes relevant (optional)++ -- visual appearance key+ , barcode :: Maybe Barcode -- ^ Barcode information (optional)+ , backgroundColor :: Maybe RGBColor -- ^ Background color of the pass (optional)+ , foregroundColor :: Maybe RGBColor -- ^ Foreground color of the pass (optional)+ , labelColor :: Maybe Text -- ^ Color of the label text. If omitted, the color is determined automatically. (optional)+ , logoText :: Maybe Text -- ^ Text displayed next to the logo on the pass (optional)+ , suppressStripShine :: Maybe Bool -- ^ If @True@, the strip image is displayed without a shine effect. (optional)++ -- web service keys+ , authenticationToken :: Maybe Text -- ^ Authentication token for use with the web service. Must be 16 characters or longer (optional)+ , webServiceURL :: Maybe Text -- ^ The URL of a web service that conforms to the API described in the Passbook Web Service Reference (optional)++ , passContent :: PassType -- ^ The kind of pass and the passes' fields (required)+}++-- * JSON instances++-- |Conditionally appends something wrapped in Maybe to a list of 'Pair'. This is necessary+-- because Passbook can't deal with null values in JSON.+(-:) :: ToJSON a => Text -> Maybe a -> ([Pair] -> [Pair])+(-:) _ Nothing = id+(-:) key (Just value) = ((key .= value) :)++$(deriveToJSON id ''PassContent)++instance ToJSON Location where+ toJSON Location{..} =+ let pairs = ("altitude" -: altitude)+ $ ("relevantText" -: relevantText)+ $ ["latitude" .= latitude+ ,"longitude" .= longitude]+ in object pairs++instance ToJSON Barcode where+ toJSON Barcode{..} =+ let pairs = ("altText" -: altText)+ $ [ "format" .= format+ , "message" .= message+ , "messageEncoding" .= messageEncoding ]+ in object pairs++instance ToJSON PassField where+ toJSON PassField{..} =+ let pairs = ("changeMessage" -: changeMessage)+ $ ("label" -: label)+ $ ("textAlignment" -: textAlignment)+ $ ("dateStyle" -: dateStyle)+ $ ("timeStyle" -: timeStyle)+ $ ("isRelative" -: isRelative)+ $ ("currencyCode" -: currencyCode)+ $ ("numberStyle" -: numberStyle)+ $ ["key" .= key, "value" .= value]+ in object pairs+++instance ToJSON Pass where+ toJSON Pass{..} =+ let pairs = ("relevantDate" -: relevantDate)+ $ ("barcode" -: barcode)+ $ ("backgroundColor" -: backgroundColor)+ $ ("foregroundColor" -: foregroundColor)+ $ ("labelColor" -: labelColor)+ $ ("logoText" -: logoText)+ $ ("suppressStripShine" -: suppressStripShine)+ $ ("authenticationToken" -: authenticationToken)+ $ ("webServiceURL" -: webServiceURL)+ $ [ "description" .= description+ , "formatVersion" .= (1 :: Int) -- Harcoding this because it should not be changed+ , "organizationName" .= organizationName+ , "passTypeIdentifier" .= passTypeIdentifier+ , "serialNumber" .= serialNumber+ , "teamIdentifier" .= teamIdentifier+ , "associatedStoreIdentifiers" .= associatedStoreIdentifiers+ , "locations" .= locations+ , (pack $ show passContent) .= passContent]+ in object pairs++-- |Internal helper function to handle Boarding Passes correctly.+getPassContent :: PassType -> PassContent+getPassContent pc = case pc of+ BoardingPass _ pc -> pc+ Coupon pc -> pc+ Event pc -> pc+ GenericPass pc -> pc+ StoreCard pc -> pc++instance ToJSON PassType where+ toJSON (BoardingPass tt PassContent{..}) = object [+ "transitType" .= tt+ , "headerFields" .= headerFields+ , "primaryFields" .= primaryFields+ , "secondaryFields" .= secondaryFields+ , "auxiliaryFields" .= auxiliaryFields+ , "backFields" .= backFields ]+ toJSON pt = toJSON $ getPassContent pt++-- |Internal helper function+renderRGB :: RGBColor -> Text+renderRGB (RGB r g b) = [st|rgb(#{show r},#{show g},#{show b})|]++instance ToJSON RGBColor where+ toJSON = toJSON . renderRGB++instance ToJSON BarcodeFormat where+ toJSON QRCode = toJSON ("PKBarcodeFormatQR" :: Text)+ toJSON PDF417 = toJSON ("PKBarcodeFormatPDF417" :: Text)++instance Show Alignment where+ show LeftAlign = "PKTextAlignmentLeft"+ show Center = "PKTextAlignmentCenter"+ show RightAlign = "PKTextAlignmentRight"+ show Natural = "PKTextAlignment"++instance ToJSON Alignment where+ toJSON = toJSON . pack . show++instance Show DateTimeStyle where+ show None = "NSDateFormatterNoStyle"+ show Short = "NSDateFormatterShortStyle"+ show Medium = "NSDateFormatterMediumStyle"+ show Long = "NSDateFormatterLongStyle"+ show Full = "NSDateFormatterFullStyle"++instance ToJSON DateTimeStyle where+ toJSON = toJSON . pack . show++instance Show NumberStyle where+ show Decimal = "PKNumberStyleDecimal"+ show Percent = "PKNumberStylePercent"+ show Scientific = "PKNumberStyleScientific"+ show SpellOut = "PKNumberStyleSpellOut"++instance ToJSON NumberStyle where+ toJSON = toJSON . pack . show+++instance Show TransitType where+ show Air = "PKTransitTypeAir"+ show Boat = "PKTransitTypeBoat"+ show Bus = "PKTransitTypeBus"+ show Train = "PKTransitTypeTrain"+ show GenericTransit = "PKTransitTypeGeneric"++instance ToJSON TransitType where+ toJSON = toJSON . pack . show++instance Show PassDate where+ show (PassDate d) =+ let timeFormat = iso8601DateFormat $ Just $ timeFmt defaultTimeLocale+ in formatTime defaultTimeLocale timeFormat d++instance ToJSON PassDate where+ toJSON = toJSON . pack . show++instance Show PassType where+ show (BoardingPass _ _) = "boardingPass"+ show (Coupon _) = "coupon"+ show (Event _) = "eventTicket"+ show (GenericPass _) = "generic"+ show (StoreCard _) = "storeCard"++-- * Auxiliary functions++-- |This function takes a 'Text' and a 'BarcodeFormat' and uses the text+-- for both the barcode message and the alternative text.+mkBarcode :: Text -> BarcodeFormat -> Barcode+mkBarcode m f = Barcode (Just m) f m "iso-8859-1"+++-- |Creates a @Just RGBColor@ if all supplied numbers are between 0 and 255.+rgb :: (Int, Int, Int) -> Maybe RGBColor+rgb (r, g, b) | isInRange r && isInRange b && isInRange b = Just $ RGB r g b+ | otherwise = Nothing+ where+ isInRange x = 0 <= x && x <= 255++-- |Creates a simple 'PassField' with just a key, a value and an optional label.+-- All the other optional fields are set to 'Nothing'.+mkSimpleField :: ToPassField a+ => Text -- ^ Key+ -> a -- ^ Value+ -> Maybe Text -- ^ Label+ -> PassField+mkSimpleField k v l = PassField Nothing k l Nothing v Nothing Nothing+ Nothing Nothing Nothing+
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ hs-pkpass.cabal view
@@ -0,0 +1,42 @@+-- Initial hs-pkpass.cabal generated by cabal init. For further +-- documentation, see http://haskell.org/cabal/users-guide/++name: hs-pkpass+version: 0.1.0.0+synopsis: A library for Passbook pass creation & signing+description: A Haskell library for type-safe creation of Passbook passes and signing through Apple's signpass tool.+homepage: https://github.com/tazjin/hs-pkpass+license: BSD3+license-file: LICENSE+author: Vincent Ambo+maintainer: geva@humac.com+-- copyright: +category: iOS+build-type: Simple+cabal-version: >=1.8++source-repository head+ type: git+ location: https://github.com/tazjin/hs-pkpass.git++source-repository this+ type: git+ location: https://github.com/tazjin/hs-pkpass/tree/v0.1+ tag: 0.1+++library+ exposed-modules: Passbook, Passbook.Types+ -- other-modules: + build-depends: base ==4.5.*,+ aeson ==0.6.*,+ conduit ==0.5.*,+ filesystem-conduit ==0.5.*,+ text ==0.11.*,+ uuid ==1.2.*,+ shelly ==0.14.*,+ directory ==1.1.*,+ random ==1.0.*,+ time ==1.4.*,+ old-locale ==1.0.*,+ shakespeare-text ==1.0.*