Now that orders are successfully being attributed to the right customers, we need to give the customers a method to provide their shipping details so the tickets and collat- erals can be dispatched to them successfully.
The first thing that’s missing is what would traditionally be referred to as a shop- ping basket or cart to hold the user’s won auctions and where they can see the items that they have pending payment.
The second component that you’ll be building is a very basic checkout form to col- lect the user’s shipping information. To achieve this we’ll be looking at another aspect of Lift, LiftScreen, which is designed specifically to make constructing forms and user input simple.
5.2.1 Implementing the basket
The basket is quite straightforward and inherits a lot of its functionality from things that have already been defined, such as the AuctionHelpers trait. The goal is to create a simple snippet that reads the contents of the database based upon which the cus- tomer is currently logged in. If there is no current customer session, it should display a friendly message requesting that the user log in to see the contents of their basket.
Once again, we’ll use the built-in TestCond snippet to present the correct con- tent depending on whether the user is logged-in or not. That will keep this logic separate from the snippet controlling basket rendering. The basket itself only has to decide whether the current basket has any contents or not. The next listing shows the Basket snippet.
import scala.xml.NodeSeq
import net.liftweb.util.Helpers._
import example.travel.model.Customer import example.travel.lib.AuctionHelpers
class Basket extends AuctionHelpers { private lazy val contents = Customer.currentUser.flatMap(
_.order.map(_.order_auctions.all)).openOr(Nil) def items = ".basket-row *" #>
contents.map(x => single(x.auction.obj)) andThen "%s ^*".format(
if(contents.isEmpty) "empty"
else "full") #> NodeSeq.Empty }
Listing 5.6 The Basket snippet
Load items from basket
B
Render items template
C
The whole Basket class inherits from AuctionHelpers so that you don’t need to rede- fine the bindings for a single Auction to obtain its name and so forth. The first thing to do is load the current user’s completed auctions from the database B. This is done by using the helper methods that were previously defined on both the Customer and OrderAuction classes earlier in this chapter. The result here is a List[OrderAuction]
if auctions exist; if the current user hasn’t won any auctions, the method returns an empty list, or Nil.
The items snippet itself C uses a different CSS-style selector that might look a little strange. This selector takes the input markup and selects a specific child node. In this case, it allows you to select either the <full> element if the contents value isn’t empty, or the <empty> node in the markup if the contents list is empty. You can find more detailed information about the available CSS-style selectors in section 6.1.2.
When it comes to the actual binding for the content, the items snippet reuses the single method from the AuctionHelpers trait that has been used elsewhere in the application.
The next listing shows the view code—note how part of the content is contained in the <full> and <empty> nodes. This allows the designer to explicitly decide what should be displayed for each scenario without needless coupling to the server- side code.
<div class="bg3 basket">
<h2>Your Basket</h2>
<lift:test_cond.loggedin>
<lift:basket.items>
<full>
<div class="basket-row">
<h3 class="name">Name</h3>
</div>
<div>
<input type="button"
name="checkout"
value="checkout" />
</div>
</full>
<empty>
<p>You have not won any auctions</p>
</empty>
</lift:basket.items>
</lift:test_cond.loggedin>
<lift:test_cond.loggedout>
<p>Please log-in to see the contents of your basket</p>
</lift:test_cond.loggedout>
<br />
</div>
Listing 5.7 Basket markup from _basket.html
Start of logged- in content
B
Invoke Basket.items snippet
C
Define template for full baskets
D
Define template for basket when empty
E
93 Implementing the basket and checkout process
Once again, this template uses the TestCond snippet to determine whether the user is logged in or not B. From there, the Basket.items snippet determines the appropriate content node in the XHTMLC. Specifically, if the basket has items, it renders the <full>
node D; otherwise it renders the content to be displayed if the user’s basket is empty E. Note that neither <empty> nor <full> are valid HTML tags; they’re simply used as markers in the template, and they’re removed by the snippet during page rendering.
The markup also defines a link to the checkout page so that users can enter their ship- ping information for that order and pay via the online payment provider PayPal.
5.2.2 Implementing the checkout
With the basket complete, the frontend needs to receive some shipping information from the customer so their tickets can be sent out. In addition, you’ll also add func- tionality to pass the customer to PayPal so they can pay for the auctions and complete the transaction.
As this section makes use of Lift’s PayPal integration, so you’ll need to make sure you add the lift-paypal artifact as a dependency to your project. Add the following line to your SBT project:
val paypal = "net.liftweb" %% "lift-paypal" % liftVersion % "compile"
Be sure to call reload and update from the SBT shell before continuing.
In the current Mapper models, there’s nowhere to hold shipping details in the database, so first you’ll need to add some extra fields to the Order model. The follow- ing listing shows the required additional fields and convenience method.
class Order extends LongKeyedMapper[Order]
with IdPK with OneToMany[Long, Order] with CreatedUpdated { ...
object shippingAddressOne
➥ extends MappedString(this,255){
override def displayName = "Address One"
} object shippingAddressTwo
➥ extends MappedString(this,255){
override def displayName = "Address Two"
} object shippingAddressCity
➥ extends MappedString(this,255){
override def displayName = "City"
} object shippingAddressPostalCode extends
➥ MappedPostalCode(this,shippingAddressCounty){
override def displayName = "Postcode"
} object shippingAddressCounty
➥ extends MappedCountry(this){
override def displayName = "Country"
} Listing 5.8 Additions to the Order model
Extra fields for shipping
B
...
def totalValue: Double = (for(
oa <- order_auctions.all;
au <- oa.auction.obj;
av <- au.currentAmount
) yield av).reduceLeft(_ + _) }
This listing defines a simple set of additional addressing fields that should be familiar to anyone who has shopped online before B. The totalValue helper method deter- mines the overall value of the auctions attributed to this order C. Essentially, this method just maps through the auctions invoking the currentAmount helper we defined in the previous chapter.
With the changes to the Order model complete, we can now focus on creating the basic checkout. The checkout process itself will consist of two screens:
■ The first to input the shipping information
■ The second to confirm the basket and shipping information, with a link to con- duct the transaction through PayPal
In order to implement the form for the shipping details, we could quite happily use a snippet and code it manually with the bind statements and so forth, exactly as we’ve done with everything else in the application. But there’s another component in Lift WebKit called LiftScreen that can help us with this.
Web applications typically have complex flows for form completion and collection of user input. To this end, LiftScreen makes building forms super simple and pro- vides a way to test forms programmatically without involving any form of HTTP simula- tion. LiftScreen also has a bigger brother called Wizard that can link lots of different screens together and control complex page flow and validation. To keep things simple here, we’ll be implementing a single screen, but making a more complex multi-screen flow based on user selection would be a simple extension of the LiftScreen code.
Wizard is covered in more detail in chapter 6.
The purpose of using LiftScreen is to ease the creation of user input flows and reduce code bulk. The following listing shows the whole LiftScreen imple- mentation for the checkout.
import net.liftweb.http.{LiftScreen,S}
import example.travel.model.{Customer,Order}
object Checkout extends LiftScreen { object order extends
ScreenVar(Customer.currentUser.flatMap(
_.order) openOr Order.create) addFields(() => order.shippingAddressOne) addFields(() => order.shippingAddressTwo) addFields(() => order.shippingAddressCity)
Listing 5.9 Input for collecting shipping details using LiftScreen Helper for order value
C
Define internal variable
B
Register specific fields
C
95 Implementing the basket and checkout process
addFields(() => order.shippingAddressPostalCode) addFields(() => order.shippingAddressCountry) def finish(){
if(order.save) S.redirectTo("summary") else S.error("Unable to save order details") } }
The first thing to say about this LiftScreen is that it sits alongside the other regular snippets in the application. LiftScreen is a subtype of DispatchSnippet, so it can be thought of as an abstraction over the normal snippets that you’re familiar with. The next point of note is that the Checkout implementation is a singleton, rather than a class like the other snippets in the application.
Within the object itself, the first item is a local ScreenVar used to hold the order instance that’s retrieved via the logged-in customer B. ScreenVars are local to the screen and can’t be shared directly. The next group of method calls C registers spe- cific model fields for receiving input in the UI. Had you wanted to construct inputs for the entire model, you could simply pass the model reference itself, and LiftScreen would automatically construct inputs for all the fields on the specified model. Finally, the finish method definition takes the order instance and saves it with the updated values from the form D. It couldn’t be easier!
Of course, it’s necessary to invoke this screen from checkout.html, but as its ren- dering methods are already plumbed in just like any other snippet; you only need to do this:
<lift:checkout />
In order to make this screen appear in an application-specific style, you can provide a customized template so that when Lift renders the screen, it does so in a manner that suits your application. The wizard and screen template you can customize is located at webapp/templates-hidden/wizard-all.html. This special template ensures that any rendering completed by either LiftScreen or its bigger brother Wizard will be styled in a manner that’s appropriate to your specific application. LiftScreen and Wizard are very powerful user input abstractions, and they save heaps of time when building user forms and workflows.
With these things in place, your input form should look something like figure 5.2.
When the user clicks the Finish button, the input is saved to the database and the user is redirected to an overview page where they can see their cart contents and its value, along with the shipping details they entered. To make this work, we need to add an OrderSummary snippet to display the shipping information and grab some summary information about the order, such as its total worth. The purpose is to let the user review what they’re purchasing, confirm their information, and give them the option to amend any details before being transferred to PayPal to collect payment.
Register specific fields
C
Finish action
D
Listing 5.10 shows the OrderSummary snippet.
import scala.xml.NodeSeq
import net.liftweb.util.Helpers._
import net.liftweb.paypal.snippet.BuyNowSnippet import example.travel.model.Customer
class OrderSummary extends BuyNowSnippet { override def dispatch = {
case "paynow" => buynow _ case "value" => value
case "shipping" => shipping }
val order = Customer.currentUser.flatMap(_.order) val amount = order.map(_.totalValue).openOr(0D) val reference = order.map(
_.reference.is.toString).openOr("n/a")
override val values = Map(
"business" -> "seller_XXXXX_biz@domain.com", "item_number" -> reference, "item_name" -> ("Auction Order: " + reference)) def value = "*" #> amount.toString
Listing 5.10 The OrderSummary snippet
Figure 5.2 User input form for collecting the shipping information
Wire up BuyNowSnippet
B
Configure PayPal
C
Get order value for auction
D
97 Implementing the basket and checkout process
def shipping = order.map { o =>
"address_one" #> o.shippingAddressOne.is &
"address_two" #> o.shippingAddressTwo.is &
"city" #> o.shippingAddressCity.is &
"postcode" #> o.shippingAddressPostalCode.is } openOr("*" #> NodeSeq.Empty) }
This class extends the Lift PayPal integration snippet, BuyNowSnippet, which provides a mechanism for automatically generating the relevant markup required to post trans- action information to PayPal. By connecting the dispatch table to the buynow method in the BuyNowSnippet B, buynow becomes callable from your template markup. In addition, the BuyNowSnippet allows you to define extra configuration parameters that will be transferred to hidden inputs in the resulting PayPal form. This is done by pro- viding a simple key-value MapC.
In addition to the PayPal setup, the value snippet method D obtains the overall value of the order using the totalValue helper method that was added to the Order model earlier in the chapter.
The shipping method reads the values that were entered in the Checkout screen and binds them for display using the familiar CSS-style selectors. Had this example been using a full Wizard, you wouldn’t need to reload the values in this way, but for the sake of simplicity, we chose to do it this way rather than complicate the example with even more new content E.
With this snippet code in place, you need to implement these snippets and add the checkout and summary pages to the sitemap. The next listing shows the markup required for the summary page.
<lift:surround with="wide" at="content">
<div>
<h2>Order Summary</h2>
<lift:basket.items>
...
</lift:basket.items>
<div class="basket-row">
<p class="bold">Total purchase value: £
<em lift="order_summary.value" /></p>
</div>
<h2>Shipping Details</h2>
<p>Details of travel will be sent to the following address: (<a href="checkout">
➥ Edit</a>)</p>
<p class="bold" lift="order_summary.shipping">
<address_one /><br/>
<address_two /><br/>
<city /><br/>
<postcode /><br/>
</p>
Listing 5.11 XHTML for checkout summary
Bind shipping info from order
E
Reuse basket markup
B
Display total value
C
Display shipping details
D
<h2>Payment</h2>
<p>Please pay for your items using the PayPal button below</p>
<lift:order_summary.paynow />
</div>
</lift:surround>
This markup should look fairly familiar. It’s just implementing the snippet methods as you’ve done in the past couple of chapters. First it reuses the Basket snippet from the previous section B, and then it displays the total order value C. In addition, it ren- ders the customer’s shipping information D and finally generates the Buy Now but- ton E, which sends the transaction to PayPal.
The only other point of note here is the static link back to the checkout page. The checkout screen is clever enough to figure out what it needs to do in order to obtain the values, so the same screen serves as both an input and editing form with no extra work. The final result should look like figure 5.3.
A couple of visual features have been added in this screenshot, such as the stage pin to indicate what phase of the transaction the user is at, but these are just standard HTML and CSS tricks, so they don’t warrant discussion here. As this screen is near the end of the user’s journey through the application, the only thing remaining is to transfer them to PayPal for payment.
Render PayPal form
E
Figure 5.3 The completed checkout summary page
99 Collecting payment with PayPal