mark: A photo of Mark kneeling on top of the Taal Volcano in the Philippines. It was a long hike. (Default)
Mark Smith ([staff profile] mark) wrote in [site community profile] changelog2009-04-14 08:07 am

[dw-free] Finishing up payment system

[commit: http://hg.dwscoalition.org/dw-free/rev/b599f8330c87]

http://bugs.dwscoalition.org/show_bug.cgi?id=116

More work on the shop interface.

Patch by [personal profile] janinedog.

Files modified:
  • bin/upgrading/en.dat
  • cgi-bin/DW/Pay.pm
  • cgi-bin/DW/Shop/Cart.pm
  • cgi-bin/DW/Shop/Item/Account.pm
  • cgi-bin/DW/Widget/PaidAccountStatus.pm
  • cgi-bin/DW/Widget/ShopCartStatusBar.pm
  • cgi-bin/DW/Widget/ShopItemGroupDisplay.pm
  • cgi-bin/LJ/Widget/ShopCart.pm
  • cgi-bin/LJ/Widget/ShopItemOptions.pm
  • htdocs/shop.bml
  • htdocs/shop.bml.text
  • htdocs/shop/account.bml
  • htdocs/shop/account.bml.text
  • htdocs/shop/cart.bml
  • htdocs/shop/cart.bml.text
  • htdocs/shop/index.bml
  • htdocs/stc/shop.css
  • htdocs/stc/widgets/shop.css
--------------------------------------------------------------------------------
diff -r 2530f9f773f5 -r b599f8330c87 bin/upgrading/en.dat
--- a/bin/upgrading/en.dat	Tue Apr 14 07:34:07 2009 +0000
+++ b/bin/upgrading/en.dat	Tue Apr 14 08:07:41 2009 +0000
@@ -2946,6 +2946,14 @@ settings.yearofbirth=Year of Birth
 
 settings.zipcode=ZIP Code
 
+shop.item.account.canbeadded.noperms=There are no more seed accounts available for purchase at this time.
+
+shop.item.account.conflicts.differentpaid=You cannot purchase two different types of paid accounts for the same person.
+
+shop.item.account.conflicts.multipleperms=You cannot purchase more than one seed account for the same person.
+
+shop.item.account.name=[[name]] ([[num]] [[?num|month|months]])
+
 sitescheme.accountlinks.account=Account
 
 sitescheme.accountlinks.btn.login=Log in
@@ -3855,6 +3863,10 @@ widget.navstripchooser.upgradetos2=<a [[
 
 widget.pagenotice.dismiss=Dismiss
 
+widget.paidaccountstatus.accounttype=Your current account type is:
+
+widget.paidaccountstatus.expiretime=Your paid time expires:
+
 widget.popularinterests.viewall=view all popular interests
 
 widget.qotd.answer=Answer
@@ -3966,6 +3978,76 @@ widget.search.username=Username
 widget.search.username=Username
 
 widget.search.yahoo=Yahoo! ID
+
+widget.shopcart.anonymous=Anonymous
+
+widget.shopcart.btn.checkout=Check Out
+
+widget.shopcart.btn.discard=Discard Entire Cart
+
+widget.shopcart.btn.removeselected=Remove Selected Items
+
+widget.shopcart.deliverydate.today=Today
+
+widget.shopcart.error.nocart=Unable to get a shopping cart for you.  Please try again later.
+
+widget.shopcart.error.noitems=You have no items in your shopping cart.
+
+widget.shopcart.header.deliverydate=Delivery Date
+
+widget.shopcart.header.from=From
+
+widget.shopcart.header.item=Item
+
+widget.shopcart.header.price=Price
+
+widget.shopcart.header.remove=Remove?
+
+widget.shopcart.header.to=To
+
+widget.shopcart.paymentmethod=Payment Method:
+
+widget.shopcart.paymentmethod.checkmoneyorder=Check/Money Order
+
+widget.shopcart.paymentmethod.paypal=PayPal
+
+widget.shopcart.total=Total:
+
+widget.shopcartstatusbar.header=Shopping Cart
+
+widget.shopcartstatusbar.itemcount=[[num]] [[?num|item|items]] for a total of [[price]]
+
+widget.shopcartstatusbar.newcart=Discard Cart
+
+widget.shopcartstatusbar.viewcart=View Cart
+
+widget.shopitemgroupdisplay.paidaccounts.header=Buy a Paid Account
+
+widget.shopitemgroupdisplay.paidaccounts.item.differentaccount=<a [[aopts]]>For a different existing account</a>
+
+widget.shopitemgroupdisplay.paidaccounts.item.exisitingaccount=<a [[aopts]]>For an existing account</a>
+
+widget.shopitemgroupdisplay.paidaccounts.item.newaccount=<a [[aopts]]>For a new account</a>
+
+widget.shopitemgroupdisplay.paidaccounts.item.self=<a [[aopts]]>For yourself</a> ([[user]])
+
+widget.shopitemoptions.error.invalidusername=The username you entered is invalid or the user does not exist.
+
+widget.shopitemoptions.error.nocart=Unable to get a shopping cart for you.  Please try again later.
+
+widget.shopitemoptions.error.notloggedin=You must be logged in as a personal account in order to purchase paid time for yourself.
+
+widget.shopitemoptions.header.paid=Paid Account
+
+widget.shopitemoptions.header.prem=Premium Paid Account
+
+widget.shopitemoptions.header.seed=Seed Account
+
+widget.shopitemoptions.highlight.seed=- Fewer than [[num]] remaining!
+
+widget.shopitemoptions.price=[[num]] [[?num|month|months]] for [[price]]
+
+widget.shopitemoptions.price.seed=Forever for [[price]]
 
 widget.support.submit.button=Submit Request
 
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Pay.pm
--- a/cgi-bin/DW/Pay.pm	Tue Apr 14 07:34:07 2009 +0000
+++ b/cgi-bin/DW/Pay.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -579,6 +579,52 @@ sub update_paid_status {
     return 1;
 }
 
+################################################################################
+# DW::Pay::num_permanent_accounts_available
+#
+# ARGUMENTS: none
+#
+# RETURN: number of permanent accounts that are still available for purchase
+#         -1 if there is no limit on how many permanent accounts can be
+#         purchased
+#
+sub num_permanent_accounts_available {
+    return 0 unless $LJ::PERMANENT_ACCOUNT_LIMIT;
+    return -1 if $LJ::PERMANENT_ACCOUNT_LIMIT < 0;
+
+    # FIXME: we need to figure out the best way to do this, which is probably
+    # to have some counter that is incremented (or decremented) whenever someone
+    # finishes the check out process with a permanent account in their cart
+    my $num_bought = 0;
+    my $num_available = $LJ::PERMANENT_ACCOUNT_LIMIT - $num_bought;
+
+    return $num_available > 0 ? $num_available : 0;
+}
+
+################################################################################
+# DW::Pay::num_permanent_accounts_available_estimated
+#
+# ARGUMENTS: none
+#
+# RETURN: estimated number of permanent accounts that are still available for
+#         purchase
+#         -1 if there is no limit on how many permanent accounts can be
+#         purchased
+#
+sub num_permanent_accounts_available_estimated {
+    my $num_available = DW::Pay::num_permanent_accounts_available();
+    return $num_available if $num_available < 1;
+
+    return 10  if $num_available <= 10;
+    return 25  if $num_available <= 25;
+    return 50  if $num_available <= 50;
+    return 100 if $num_available <= 100;
+    return 150 if $num_available <= 150;
+    return 200 if $num_available <= 200;
+    return 300 if $num_available <= 300;
+    return 400 if $num_available <= 400;
+    return 500;
+}
 
 ################################################################################
 ################################################################################
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Shop/Cart.pm
--- a/cgi-bin/DW/Shop/Cart.pm	Tue Apr 14 07:34:07 2009 +0000
+++ b/cgi-bin/DW/Shop/Cart.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -143,17 +143,31 @@ sub save {
 }
 
 
+# returns the number of items in this cart
+sub num_items {
+    my $self = $_[0];
+
+    return scalar @{ $self->{items} || [] };
+}
+
+
 # returns 1/0 if this cart has any items in it
 sub has_items {
     my $self = $_[0];
 
-    return scalar( @{ $self->{items} || [] } ) > 0 ? 1 : 0;
+    return $self->num_items > 0 ? 1 : 0;
 }
 
 
 # add an item to the shopping cart, returns 1/0
 sub add_item {
     my ( $self, $item ) = @_;
+
+    # make sure this item is allowed to be added
+    my $error;
+    unless ( $item->can_be_added( errref => \$error ) ) {
+        return ( 0, $error );
+    }
 
     # iterate over existing items to see if any conflict
     foreach my $it ( @{$self->items} ) {
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Shop/Item/Account.pm
--- a/cgi-bin/DW/Shop/Item/Account.pm	Tue Apr 14 07:34:07 2009 +0000
+++ b/cgi-bin/DW/Shop/Item/Account.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -6,6 +6,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -26,29 +27,30 @@ sub new {
     my $type = delete $args{type};
     return undef unless exists $LJ::SHOP{$type};
 
-    # at this point, there needs to be only one argument, and it needs to be one
-    # of the target types
-    return undef unless
-        scalar( keys %args ) == 1 &&
-            ( $args{target_username} || $args{target_userid} || $args{target_email} );
+    # from_userid will be 0 if the sender isn't logged in
+    return undef unless $args{from_userid} == 0 || LJ::load_userid( $args{from_userid} );
 
     # now do validation.  since new is only called when the item is being added
     # to the shopping cart, then we are comfortable doing all of these checks
     # on things at the time this item is put together
-    if ( my $un = $args{target_username} ) {
-        # username needs to be valid and not exist
-        return undef unless $un = LJ::canonical_username( $un );
-        return undef if LJ::load_user( $un );
-
-        $args{target_username} = $un;
-
-    } elsif ( my $uid = $args{target_userid} ) {
+    if ( my $uid = $args{target_userid} ) {
         # userid needs to exist
         return undef unless LJ::load_userid( $uid );
+    } elsif ( my $email = $args{target_email} ) {
+        # email address must be valid
+        my @email_errors;
+        LJ::check_email( $email, \@email_errors );
+        return undef if @email_errors;
+    } else {
+        return undef;
+    }
 
-    } elsif ( my $email = $args{target_email} ) {
-        # FIXME: validate email address
+    if ( $args{deliverydate} ) {
+        return undef unless $args{deliverydate} =~ /^\d\d\d\d-\d\d-\d\d$/;
+    }
 
+    if ( $args{anonymous} ) {
+        return undef unless $args{anonymous} == 1;
     }
 
     # looks good
@@ -93,6 +95,22 @@ sub unapply {
 }
 
 
+# returns 1 if this item is allowed to be added to the shopping cart
+sub can_be_added {
+    my ( $self, %opts ) = @_;
+
+    my $errref = $opts{errref};
+
+    # check to see if we're over the permanent account limit
+    if ( $self->permanent && DW::Pay::num_permanent_accounts_available() < 1 ) {
+        $$errref = LJ::Lang::ml( 'shop.item.account.canbeadded.noperms' );
+        return 0;
+    }
+
+    return 1;
+}
+
+
 # given another item, see if that item conflicts with this item (i.e.,
 # if you can't have both in your shopping cart at the same time).
 #
@@ -101,20 +119,20 @@ sub conflicts {
     my ( $self, $item ) = @_;
 
     # first see if we're talking about the same target
-    # FIXME: maybe not include email here, what happens if they want to buy 3 paid accounts
-    # and send them to the same email address?
+    # note that we're not checking email here because they may want to buy
+    # multiple paid accounts and send them to all to the same email address
+    # (so they can can create multiple new paid accounts)
     return if
-        ( $self->t_userid   && $self->t_userid   != $item->t_userid   ) ||
-        ( $self->t_email    && $self->t_email    != $item->t_email    ) ||
-        ( $self->t_username && $self->t_username != $item->t_username );
+        ( $self->t_userid && ( $self->t_userid != $item->t_userid ) ) ||
+        ( $self->t_email                                            );
 
-    # target same, if either is permanent, then fail because
+    # target same, if both are permanent, then fail because
     # THERE CAN BE ONLY ONE
-    return 'Already purchasing a permanent account for this target.'
-        if $self->permanent || $item->permanent;
+    return LJ::Lang::ml( 'shop.item.account.conflicts.multipleperms' )
+        if $self->permanent && $item->permanent;
 
     # otherwise ensure that the classes are the same
-    return 'Already chose to upgrade to a ' . $self->class . ', do not do both!'
+    return LJ::Lang::ml( 'shop.item.account.conflicts.differentpaid' )
         if $self->class ne $item->class;
 
     # guess we allow it
@@ -132,12 +150,6 @@ sub t_html {
             if $u;
         return "<strong>invalid userid $uid</strong>";
 
-    } elsif ( my $user = $self->t_username ) {
-        my $u = LJ::load_user( $user );
-        return $u->ljuser_display
-            if $u;
-        return "<strong>$user</strong>";
-
     } elsif ( my $email = $self->t_email ) {
         return "<strong>$email</strong>";
 
@@ -147,6 +159,23 @@ sub t_html {
 }
 
 
+# render the item name as a string
+sub name_html {
+    my $self = $_[0];
+
+    my $name = "invalid name";
+    foreach my $cap ( keys %LJ::CAP ) {
+        if ( $LJ::CAP{$cap} && $LJ::CAP{$cap}->{_account_type} eq $self->class ) {
+            $name = $LJ::CAP{$cap}->{_visible_name};
+            last;
+        }
+    }
+
+    return $name if $self->permanent;
+    return LJ::Lang::ml( 'shop.item.account.name', { name => $name, num => $self->months } );
+}
+
+
 # this is a getter/setter so it is pulled out
 sub id {
     return $_[0]->{id} unless defined $_[1];
@@ -155,14 +184,16 @@ sub id {
 
 
 # simple accessors
-sub applied    { return $_[0]->{applied};         }
-sub cost       { return $_[0]->{cost};            }
-sub months     { return $_[0]->{months};          }
-sub class      { return $_[0]->{class};           }
-sub t_userid   { return $_[0]->{target_userid};   }
-sub t_email    { return $_[0]->{target_email};    }
-sub t_username { return $_[0]->{target_username}; }
-sub permanent  { return $_[0]->months == 99;      }
+sub applied      { return $_[0]->{applied};         }
+sub cost         { return $_[0]->{cost};            }
+sub months       { return $_[0]->{months};          }
+sub class        { return $_[0]->{class};           }
+sub t_userid     { return $_[0]->{target_userid};   }
+sub t_email      { return $_[0]->{target_email};    }
+sub permanent    { return $_[0]->months == 99;      }
+sub from_userid  { return $_[0]->{from_userid};     }
+sub deliverydate { return $_[0]->{deliverydate};    }
+sub anonymous    { return $_[0]->{anonymous};       }
 
 
 1;
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Widget/PaidAccountStatus.pm
--- a/cgi-bin/DW/Widget/PaidAccountStatus.pm	Tue Apr 14 07:34:07 2009 +0000
+++ b/cgi-bin/DW/Widget/PaidAccountStatus.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -6,6 +6,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -23,10 +24,8 @@ use DW::Pay;
 use DW::Pay;
 use DW::Shop;
 
-# general purpose shop CSS used by the entire shop system
-sub need_res { qw( stc/widgets/shop.css ) }
+sub need_res { qw( stc/shop.css ) }
 
-# main renderer for this particular thingy
 sub render_body {
     my ( $class, %opts ) = @_;
 
@@ -36,15 +35,13 @@ sub render_body {
     my $account_type = DW::Pay::get_account_type_name( $remote );
     my $expires_at = DW::Pay::get_account_expiration_time( $remote );
     my $expires_on = $expires_at > 0
-                     ? 'Your paid time expires: ' . LJ::mysql_time( $expires_at )
+                     ? "<br />" . $class->ml( 'widget.paidaccountstatus.expiretime' ) . " " . LJ::mysql_time( $expires_at )
                      : '';
 
-    my $ret = qq{
-<div class='shop-account-status'>
-    Your current account type is: <strong>$account_type</strong><br />
-    $expires_on
-</div>
-    };
+    my $ret = "<div class='shop-account-status'>";
+    $ret .= $class->ml( 'widget.paidaccountstatus.accounttype' ) . " ";
+    $ret .= "<strong>$account_type</strong>$expires_on";
+    $ret .= "</div>";
 
     return $ret;
 }
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Widget/ShopCartStatusBar.pm
--- a/cgi-bin/DW/Widget/ShopCartStatusBar.pm	Tue Apr 14 07:34:07 2009 +0000
+++ b/cgi-bin/DW/Widget/ShopCartStatusBar.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -6,6 +6,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -22,10 +23,8 @@ use Carp qw/ croak /;
 
 use DW::Shop;
 
-# general purpose shop CSS used by the entire shop system
-sub need_res { qw( stc/widgets/shop.css ) }
+sub need_res { qw( stc/shop.css ) }
 
-# main renderer for this particular thingy
 sub render_body {
     my ( $class, %opts ) = @_;
 
@@ -37,19 +36,21 @@ sub render_body {
     # old cart is gone with the wind ...
     my $cart = $opts{newcart} ? DW::Shop::Cart->new_cart( $u ) : $shop->cart;
 
-    # if minimal, and the cart is empty, bail
-    return if $opts{minimal} && ! $cart->has_items;
+    # if the cart is empty, bail
+    return unless $cart->has_items;
 
     # render out information about this cart
-    my $ret = '[ ';
-    $ret .= 'Shopping Cart for ' . ( $u ? $u->ljuser_display : 'anonymous user' );
-    $ret .= '; cartid = ' . $cart->id;
-    $ret .= ' created ' . LJ::ago_text( $cart->age );
-    $ret .= '; total = $' . $cart->display_total;
-    $ret .= '; <a href="/shop?newcart=1">make new cart</a>';
-    $ret .= '; <a href="/shop/cart">view cart</a>';
-    $ret .= '; <a href="/shop/checkout">checkout</a>';
-    $ret .= ' ]';
+    my $ret = "<div class='shop-cart-status'>";
+    $ret .= "<strong>" . $class->ml( 'widget.shopcartstatusbar.header' ) . "</strong><br />";
+    $ret .= $class->ml( 'widget.shopcartstatusbar.itemcount', { num => $cart->num_items, price => '$' . $cart->display_total . " USD" } );
+    $ret .= "<br />";
+
+    $ret .= "<ul>";
+    $ret .= "<li><a href='$LJ::SITEROOT/shop/cart'><strong>" . $class->ml( 'widget.shopcartstatusbar.viewcart' ) . "</strong></a></li>";
+    $ret .= "<li><a href='$LJ::SITEROOT/shop?newcart=1'><strong>" . $class->ml( 'widget.shopcartstatusbar.newcart' ) . "</strong></a></li>";
+    $ret .= "</ul>";
+
+    $ret .= "</div>";
 
     return $ret;
 }
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/DW/Widget/ShopItemGroupDisplay.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Widget/ShopItemGroupDisplay.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,47 @@
+#!/usr/bin/perl
+#
+# DW::Widget::ShopItemGroupDisplay
+#
+# Renders a group of shop items for display on the first page of the shop.
+#
+# Authors:
+#      Janine Costanzo <janine@netrophic.com>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Widget::ShopItemGroupDisplay;
+
+use strict;
+use base qw/ LJ::Widget /;
+use Carp qw/ croak /;
+
+sub need_res { qw( stc/shop.css ) }
+
+sub render_body {
+    my ( $class, %opts ) = @_;
+
+    my $remote = LJ::get_remote();
+    my $ret;
+
+    if ( $opts{group} eq 'paidaccounts' ) {
+        $ret .= "<h2>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.header' ) . "</h2>";
+        $ret .= "<ul>";
+        if ( $remote && $remote->is_personal ) {
+            $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.self', { aopts => "href='$LJ::SITEROOT/shop/account?for=self'", user => $remote->ljuser_display } ) . "</li>";
+            $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.differentaccount', { aopts => "href='$LJ::SITEROOT/shop/account?for=gift'" } ) . "</li>";
+        } else {
+            $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.exisitingaccount', { aopts => "href='$LJ::SITEROOT/shop/account?for=gift'" } ) . "</li>";
+        }
+        $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.newaccount', { aopts => "href='$LJ::SITEROOT/shop/account?for=new'" } ) . "</li>";
+        $ret .= "</ul>";
+    }
+
+    return $ret;
+}
+
+1;
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/LJ/Widget/ShopCart.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/LJ/Widget/ShopCart.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,115 @@
+#!/usr/bin/perl
+#
+# LJ::Widget::ShopCart
+#
+# Returns the current shopping cart for the remote user.
+#
+# Authors:
+#      Janine Costanzo <janine@netrophic.com>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package LJ::Widget::ShopCart;
+
+use strict;
+use base qw/ LJ::Widget /;
+use Carp qw/ croak /;
+
+sub need_res { qw( stc/shop.css ) }
+
+sub render_body {
+    my ( $class, %opts ) = @_;
+
+    my $ret;
+
+    my $cart = DW::Shop->get->cart
+        or return $class->ml( 'widget.shopcart.error.nocart' );
+
+    return $class->ml( 'widget.shopcart.error.noitems' )
+        unless @{$cart->items};
+
+    $ret .= $class->start_form;
+
+    $ret .= "<table class='shop-cart'>";
+    $ret .= "<tr><th>" . $class->ml( 'widget.shopcart.header.remove' ) . "</th>";
+    $ret .= "<th>" . $class->ml( 'widget.shopcart.header.item' ) . "</th>";
+    $ret .= "<th>" . $class->ml( 'widget.shopcart.header.deliverydate' ) . "</th>";
+    $ret .= "<th>" . $class->ml( 'widget.shopcart.header.to' ) . "</th>";
+    $ret .= "<th>" . $class->ml( 'widget.shopcart.header.from' ) . "</th>";
+    $ret .= "<th>" . $class->ml( 'widget.shopcart.header.price' ) . "</th></tr>";
+    foreach my $item ( @{$cart->items} ) {
+        my $from_u = LJ::load_userid( $item->from_userid );
+
+        $ret .= "<tr>";
+        $ret .= "<td>" . $class->html_check( name => 'remove_' . $item->id, value => 1 ) . "</td>";
+        $ret .= "<td>" . $item->name_html . "</td>";
+        $ret .= "<td>" . ( $item->deliverydate ? $item->deliverydate : $class->ml( 'widget.shopcart.deliverydate.today' ) ) . "</td>";
+        $ret .= "<td>" . $item->t_html . "</td>";
+        $ret .= "<td>" . ( $item->anonymous || !LJ::isu( $from_u ) ? $class->ml( 'widget.shopcart.anonymous' ) : $from_u->ljuser_display ) . "</td>";
+        $ret .= "<td>\$" . $item->cost . " USD</td>";
+        $ret .= "</tr>";
+    }
+    $ret .= "<tr><td colspan='6' class='total'>" . $class->ml( 'widget.shopcart.total' ) . " \$" . $cart->display_total . " USD</td></tr>";
+    $ret .= "</table>";
+
+    $ret .= "<div class='shop-cart-btn'>";
+
+    $ret .= "<p>" . $class->html_submit( removeselected => $class->ml( 'widget.shopcart.btn.removeselected' ) ) . " ";
+    $ret .= $class->html_submit( discard => $class->ml( 'widget.shopcart.btn.discard' ) ) . "</p>";
+
+    my @paypal_option = ( paypal => $class->ml( 'widget.shopcart.paymentmethod.paypal' ) )
+        if keys %LJ::PAYPAL_CONFIG;
+    $ret .= "<p>" . $class->ml( 'widget.shopcart.paymentmethod' ) . " ";
+    $ret .= $class->html_select(
+        name => 'paymentmethod',
+        selected => keys %LJ::PAYPAL_CONFIG ? 'paypal' : 'checkmoneyorder',
+        list => [
+            @paypal_option,
+            checkmoneyorder => $class->ml( 'widget.shopcart.paymentmethod.checkmoneyorder' ),
+        ],
+    ) . " ";
+    $ret .= $class->html_submit( checkout => $class->ml( 'widget.shopcart.btn.checkout' ) ) . "</p>";
+
+    $ret .= "</div>";
+
+    $ret .= $class->end_form;
+
+    return $ret;
+}
+
+sub handle_post {
+    my ( $class, $post, %opts ) = @_;
+
+    # check out
+    if ( $post->{checkout} ) {
+        my $method = 'paypal';
+        $method = 'checkmoneyorder' if $post->{paymentmethod} eq 'checkmoneyorder' || !keys %LJ::PAYPAL_CONFIG;
+
+        return BML::redirect( "$LJ::SITEROOT/shop/checkout?method=$method" );
+    }
+
+    # remove selected items
+    if ( $post->{removeselected} ) {
+        my $cart = DW::Shop->get->cart
+            or return ( error => $class->ml( 'widget.shopcart.error.nocart' ) );
+
+        foreach my $val ( keys %$post ) {
+            next unless $post->{$val} && $val =~ /^remove_(\d+)$/;
+            $cart->remove_item( $1 );
+        }
+    }
+
+    # discard entire cart
+    if ( $post->{discard} ) {
+        return BML::redirect( "$LJ::SITEROOT/shop?newcart=1" );
+    }
+
+    return;
+}
+
+1;
diff -r 2530f9f773f5 -r b599f8330c87 cgi-bin/LJ/Widget/ShopItemOptions.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/LJ/Widget/ShopItemOptions.pm	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,137 @@
+#!/usr/bin/perl
+#
+# LJ::Widget::ShopItemOptions
+#
+# Returns the options for purchasing a particular shop item.
+#
+# Authors:
+#      Janine Costanzo <janine@netrophic.com>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself.  For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package LJ::Widget::ShopItemOptions;
+
+use strict;
+use base qw/ LJ::Widget /;
+use Carp qw/ croak /;
+
+sub need_res { qw( stc/shop.css ) }
+
+sub render_body {
+    my ( $class, %opts ) = @_;
+
+    my $remote = LJ::get_remote();
+    my $ret;
+
+    my $option_name = $opts{option_name};
+    my $given_item = $opts{item};
+
+    return "" unless $option_name && $given_item;
+
+    # get all of the possible month values for the item
+    # note that it's okay if there's no month values for an item;
+    # we'll just print the item itself in that case
+    my @month_values;
+    foreach my $item ( keys %LJ::SHOP ) {
+        if ( $item =~ /^$given_item(\d*)$/ ) {
+            push @month_values, $1;
+        }
+    }
+
+    $ret .= "<strong>" . $class->ml( "widget.shopitemoptions.header.$given_item" ) . "</strong>";
+
+    my $num_perms = DW::Pay::num_permanent_accounts_available_estimated();
+    if ( $num_perms > 0 ) {
+        my $highlight_string = $class->ml( "widget.shopitemoptions.highlight.$given_item", { num => $num_perms } );
+        $ret .= " <span class='shop-item-highlight'>$highlight_string</span>"
+            unless $highlight_string eq 'ShopItemOptions';
+    }
+
+    $ret .= "<br />";
+
+    foreach my $month_value ( sort { $a <=> $b } @month_values ) {
+        my $full_item = $given_item . $month_value;
+        if ( ref $LJ::SHOP{$full_item} eq 'ARRAY' ) {
+            my $price_string = $class->ml( "widget.shopitemoptions.price.$full_item", { price => "\$$LJ::SHOP{$full_item}->[0] USD" } );
+            $price_string = $class->ml( 'widget.shopitemoptions.price', { num => $month_value, price => "\$$LJ::SHOP{$full_item}->[0] USD" } )
+                if $price_string eq 'ShopItemOptions';
+
+            $ret .= $class->html_check(
+                type => 'radio',
+                name => $option_name,
+                id => $full_item,
+                value => $full_item,
+            ) . " <label for='$full_item'>$price_string</label><br />";
+        }
+    }
+
+    return $ret;
+}
+
+sub handle_post {
+    my ( $class, $post, %opts ) = @_;
+
+    # now try to add this item to their list
+    my $cart = DW::Shop->get->cart
+        or return ( error => $class->ml( 'widget.shopitemoptions.error.nocart' ) );
+
+    my %item_data;
+
+    my $remote = LJ::get_remote();
+    $item_data{from_userid} = $remote ? $remote->id : 0;
+
+    if ( $post->{for} eq 'self' ) {
+        if ( $remote && $remote->is_personal ) {
+            $item_data{target_userid} = $remote->id;
+        } else {
+            return ( error => $class->ml( 'widget.shopitemoptions.error.notloggedin' ) );
+        }
+    } elsif ( $post->{for} eq 'gift' ) {
+        my $target_u = LJ::load_user( $post->{username} );
+        if ( LJ::isu( $target_u ) ) {
+            $item_data{target_userid} = $target_u->id;
+        } else {
+            return ( error => $class->ml( 'widget.shopitemoptions.error.invalidusername' ) );
+        }
+    } elsif ( $post->{for} eq 'new' ) {
+        my @email_errors;
+        LJ::check_email( $post->{email}, \@email_errors );
+        if ( @email_errors ) {
+            return ( error => join( ', ', @email_errors ) );
+        } else {
+            $item_data{target_email} = $post->{email};
+        }
+    }
+
+    if ( $post->{deliverydate_mm} && $post->{deliverydate_dd} && $post->{deliverydate_yyyy} ) {
+        my $given_date = DateTime->new(
+            year => $post->{deliverydate_yyyy}+0,
+            month => $post->{deliverydate_mm}+0,
+            day => $post->{deliverydate_dd}+0,
+        );
+
+        $item_data{deliverydate} = $given_date->date
+            unless $given_date->date eq DateTime->today->date;
+    }
+
+    $item_data{anonymous} = 1
+        if $post->{anonymous} || !$remote;
+
+    # build a new item and try to toss it in the cart.  this fails if there's a
+    # conflict or something
+    if ( $post->{accttype} ) {
+        my ( $rv, $err ) = $cart->add_item(
+            DW::Shop::Item::Account->new( type => $post->{accttype}, %item_data )
+        );
+        return ( error => $err ) unless $rv;
+    }
+
+    return;
+}
+
+1;
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop.bml
--- a/htdocs/shop.bml	Tue Apr 14 07:34:07 2009 +0000
+++ b/htdocs/shop.bml	Tue Apr 14 08:07:41 2009 +0000
@@ -7,6 +7,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -21,41 +22,30 @@ body<=
     use strict;
     use vars qw/ %GET %POST $title /;
 
+    # WE ARE NOT OPEN FOR BUSINESS
+    return BML::redirect( "$LJ::SITEROOT/" )
+        unless $LJ::IS_DEV_SERVER;
+
     # this page uses new style JS
-    LJ::need_res( 'stc/widget/shop.css' );
+    LJ::need_res( 'stc/shop.css' );
     LJ::set_active_resource_group( 'jquery' );
 
-    # the basic shop page is a collection of widgets!  thanks Janine :)
-    return DW::Widget::ShopCartStatusBar->render( %GET, minimal => 1 );
+    $title = BML::ml( '.title', { sitename => $LJ::SITENAMESHORT } );
+
+    my $ret;
+
+    $ret .= DW::Widget::ShopCartStatusBar->render( %GET );
+    $ret .= "<p>" . BML::ml( '.intro', { sitename => $LJ::SITENAMESHORT } ) . "</p>";
+
+    $ret .= DW::Widget::ShopItemGroupDisplay->render( group => 'paidaccounts' );
+
+    $ret .= "<div class='shopbox'>";
+    $ret .= "<p>" . BML::ml( '.sideblurb', { sitename => $LJ::SITENAMESHORT, aopts => "href='$LJ::HELPURL{paidaccountinfo}'" } ). "</p>";
+    $ret .= "</div>";
+
+    return $ret;
 }
 _code?>
-
-
-<?p Welcome to the Dreamwidth store!  If you are interested in supporting Dreamwidth Studios
-or are just looking for more features for your account, you have come to the right place. 
-I admit this page is pretty ugly and hope that someone will fix it. p?>
-
-<div id='left' class='shopbox'>
-    <?p If you are looking for a paid account, you pretty much have three options... p?>
-    <ul>
-        <li><a href='<?siteroot?>/shop/account?for=self'>...for yourself</a></li>
-        <li><a href='<?siteroot?>/shop/account?for=gift'>...for a gift</a></li>
-        <li><a href='<?siteroot?>/shop/account?for=new'>...a new account</a></li>
-    </ul>
-</div>
-
-<div id='right' class='shopbox'>
-    <?p This box has some information about Dreamwidth, a promotional blurb or
-    some other thing to tell you about us. p?>
-    <?p There is likely going to be a really compelling bunch of text here, but darn
-    if someone else has to write it. (And English strip it.) p?>
-    <?p Well, please enjoy Dreamwidth! p?>
-</div>
-
-
-
-
-
 <=body
-title=><?_ml .title _ml?>
+title=><?_code return $title; _code?>
 page?>
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop.bml.text
--- a/htdocs/shop.bml.text	Tue Apr 14 07:34:07 2009 +0000
+++ b/htdocs/shop.bml.text	Tue Apr 14 08:07:41 2009 +0000
@@ -1,4 +1,7 @@
 ;; -*- coding: utf-8 -*-
-.title=The Store
 
+.intro=Welcome to the [[sitename]] shop!  If you are interested in supporting [[sitename]] or are just looking for more features for your account, you have come to the right place.
 
+.sideblurb=You can learn about paid accounts <a [[aopts]]>here</a>.
+
+.title=[[sitename]] Shop
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop/account.bml
--- a/htdocs/shop/account.bml	Tue Apr 14 07:34:07 2009 +0000
+++ b/htdocs/shop/account.bml	Tue Apr 14 08:07:41 2009 +0000
@@ -7,6 +7,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -22,7 +23,7 @@ body<=
     use vars qw/ %GET %POST $title /;
 
     # this page uses new style JS
-    LJ::need_res( 'stc/widget/shop.css' );
+    LJ::need_res( 'stc/shop.css' );
     LJ::set_active_resource_group( 'jquery' );
 
     # let's see what they're trying to do
@@ -30,120 +31,100 @@ body<=
     return BML::redirect( "$LJ::SITEROOT/shop" )
         unless $for && $for =~ /^(?:self|gift|new)$/;
 
+    $title = $ML{'.title'};
+
     # ensure they have a user if it's for self
     my $remote = LJ::get_remote();
-    return 'need a remote boss!'
-        if $for eq 'self' && ! $remote;
+    return $ML{'.error.invalidself'}
+        if $for eq 'self' && ( !$remote || !$remote->is_personal );
 
-    # setup the output
     my $ret = DW::Widget::ShopCartStatusBar->render( %GET );
-    $ret .= qq{
-<div class="leftybox">Yep, this is the page where you buy a paid account.  We could put some really awesome
-text in this box to tell you what about paid accounts is awesome.</div>
-    };
 
-    # show account status box
-    $ret .= DW::Widget::PaidAccountStatus->render;
+    $ret .= "<p><a href='$LJ::SITEROOT/shop'>&lt;&lt; " . BML::ml( '.backlink', { sitename => $LJ::SITENAMESHORT } ) . "</a></p>";
 
-    # if they posted...
-    my $try_post = sub {
-        return 'Faiiiiiil'
+    if ( $for eq 'self' ) {
+        $ret .= "<div class='leftybox'>" . BML::ml( '.intro.self', { user => $remote->ljuser_display } ) . "</div>";
+        $ret .= DW::Widget::PaidAccountStatus->render;
+    } elsif ( $for eq 'gift' ) {
+        $ret .= "<p>$ML{'.intro.gift'}</p>";
+    } else { # $for eq 'new'
+        $ret .= "<p>$ML{'.intro.new'}</p>";
+    }
+
+    if ( LJ::did_post() ) {
+        return "<?h1 $ML{'Error'} h1?><?p $ML{'error.invalidform'} p?>"
             unless LJ::check_form_auth();
 
-        my $at = $POST{accttype};
-        return 'You must select an account type'
-            unless $at && exists $LJ::SHOP{$at};
-
-        # now try to add this item to their list
-        my $cart = DW::Shop->get->cart
-            or return 'Failed to get a shopping cart for you, please try again later.';
-
-        my %who_for;
-        if ( $for eq 'self' ) {
-            my $remote = LJ::get_remote()
-                or return '<?needlogin?>';
-            $who_for{target_userid} = $remote->id;
-
-        } elsif ( $for eq 'gift' ) {
-            # FIXME: try to validate the email address
-            $who_for{target_email} = $POST{str};
-
-        } elsif ( $for eq 'new' ) {
-            my $un = LJ::canonical_username( $POST{str} );
-            return 'Invalid username'
-                unless $un;
-            return 'Username already in use'
-                if LJ::load_user( $un );
-
-            # FIXME: also, we should put a hold on this username to prevent people from
-            # doubling up on a purchase
-            $who_for{target_username} = $un;
-
+        my $error;
+        my $post_fields = LJ::Widget::ShopItemOptions->post_fields( \%POST );
+        if ( keys %$post_fields ) { # make sure the user selected an account type
+            # need to do this because all of these form fields are in the BML page instead of in the widget
+            LJ::Widget->use_specific_form_fields( post => \%POST, widget => "ShopItemOptions", fields => [ qw( for username email deliverydate_mm deliverydate_dd deliverydate_yyyy anonymous ) ] );
+            my %from_post = LJ::Widget->handle_post( \%POST, ( 'ShopItemOptions' ) );
+            $error = $from_post{error} if $from_post{error};
+        } else {
+            $error = $ML{'.error.noselection'};
         }
 
-        # build a new item and try to toss it in the cart.  this fails if there's a
-        # conflict or something
-        my ( $rv, $err ) = $cart->add_item(
-            DW::Shop::Item::Account->new( type => $at, %who_for )
-        );
-        return $err unless $rv;
-
-        # to make this a gift for another account, simply change what target_userid
-        # is set to
-
-        # to make this something to be emailed to someone, set target_email
-
-        # to make this a purchase of a new account, set target_username
-
-        # since we updated their list, return them to this page
-        return BML::redirect( "$LJ::SITEROOT/shop/account?for=$for" );
-    };
-    if ( LJ::did_post() ) {
-        my $errs = $try_post->();
-        if ( $errs ) {
-            $ret .= qq{<div class="shop-error">$errs</div>};
+        if ( $error ) {
+            $ret .= qq{<div class="shop-error">$error</div>};
+        } else {
+            return BML::redirect( "$LJ::SITEROOT/shop/cart" );
         }
     }
 
-    # all done
+    $ret .= "<div style='clear: both;'></div>";
+    $ret .= "<form method='post'>";
+    $ret .= LJ::form_auth();
+    $ret .= "<table class='shop-table'><tr><td>";
+    $ret .= LJ::Widget::ShopItemOptions->render( option_name => 'accttype', item => 'prem' );
+    $ret .= "</td><td>";
+    $ret .= LJ::Widget::ShopItemOptions->render( option_name => 'accttype', item => 'paid' );
+    $ret .= "</tr>";
+
+    if ( DW::Pay::num_permanent_accounts_available() > 0 ) {
+        $ret .= "<tr><td colspan='2'>";
+        $ret .= LJ::Widget::ShopItemOptions->render( option_name => 'accttype', item => 'seed' );
+        $ret .= "</td></tr>";
+    }
+
+    $ret .= "</table>";
+
+    if ( $for =~ /^(?:gift|new)$/ ) {
+        $ret .= "<table class='shop-table-gift'>";
+
+        if ( $for eq 'gift' ) {
+            $ret .= "<tr><td>$ML{'.giftfor.username'}</td><td>" . LJ::html_text( { name => 'username' } ) . "</td></tr>";
+        } else { # $for eq 'new'
+            $ret .= "<tr><td>$ML{'.giftfor.email'}</td><td>" . LJ::html_text( { name => 'email' } ) . "</td></tr>";
+        }
+
+        $ret .= "<tr><td>$ML{'.giftfor.deliverydate'}</td>";
+        $ret .= "<td>" . LJ::html_datetime( {
+            name => 'deliverydate',
+            default => DateTime->today->date,
+            notime => 1,
+        } ) . "</td></tr>";
+        $ret .= "<tr><td>$ML{'.giftfor.anonymous'}</td>";
+        $ret .= "<td>" . LJ::html_check( {
+            name => 'anonymous',
+            value => 1,
+            selected => $remote ? 0 : 1,
+            disabled => $remote ? 0 : 1,
+        } ) . "</td></tr>";
+
+        $ret .= "</table>";
+    }
+
+    $ret .= LJ::html_hidden( for => $GET{for} );
+    $ret .= "<p>" . LJ::html_submit( $ML{'.btn.addtocart'} ) . "</p>";
+    $ret .= "</form>";
+
+    $ret .= "<p><a href='$LJ::SITEROOT/shop'>&lt;&lt; " . BML::ml( '.backlink', { sitename => $LJ::SITENAMESHORT } ) . "</a></p>";
+
     return $ret;
 }
 _code?>
-
-<div style='clear: both;'></div>
-<form method='post'>
-<?_code return LJ::form_auth(); _code?>
-<table class='shop-table'>
-<tr><td>
-
-<strong>Premium Paid Account</strong><br />
-<input type='radio' name='accttype' id='prem6' value='prem6'><label for='prem6'>6 months for $20 USD</label></input><br />
-<input type='radio' name='accttype' id='prem12' value='prem12'><label for='prem12'>1 year for $40 USD</label></input>
-
-</td><td>
-
-<strong>Paid Account</strong><br />
-<input type='radio' name='accttype' id='paid1' value='paid1'><label for='paid1'>1 month for $3 USD</label></input><br />
-<input type='radio' name='accttype' id='paid2' value='paid2'><label for='paid2'>2 months for $5 USD</label></input><br />
-<input type='radio' name='accttype' id='paid6' value='paid6'><label for='paid6'>6 months for $13 USD</label></input><br />
-<input type='radio' name='accttype' id='paid12' value='paid12'><label for='paid12'>1 year for $25 USD</label></input>
-
-</td><td>
-
-<strong>Seed Account</strong><br />
-<input type='radio' name='accttype' id='seed' value='seed'><label for='seed'>Forever for $200 USD</label></input>
-
-</td></tr>
-</table>
-
-username or email: <input type='text' name='str' /><br />
-<input type='submit' /><br />
-
-<?p Dear Janine, please add the rest of the page here. p?>
-
-
-</form>
-
 <=body
-title=>Buy Paid Time
+title=><?_code return $title; _code?>
 page?>
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop/account.bml.text
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/account.bml.text	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,25 @@
+;; -*- coding: utf-8 -*-
+
+.backlink=Back to [[sitename]] Shop
+
+.btn.addtocart=Add to Order
+
+.error.invalidself=You must be logged in as a personal account in order to purchase paid time for yourself.
+
+.error.noselection=You must select an item to add to your cart.
+
+.giftfor.anonymous=Anonymous gift?
+
+.giftfor.deliverydate=Delivery date:
+
+.giftfor.email=Email address to receive the account creation code for this account:
+
+.giftfor.username=Username to receive this account:
+
+.intro.gift=Please choose the type of paid account that you'd like to purchase for an existing account.
+
+.intro.new=Please choose the type of paid account that you'd like to purchase for a new account.
+
+.intro.self=Please choose the type of paid account that you'd like to purchase for your account [[user]].
+
+.title=Buy a Paid Account
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop/cart.bml
--- a/htdocs/shop/cart.bml	Tue Apr 14 07:34:07 2009 +0000
+++ b/htdocs/shop/cart.bml	Tue Apr 14 08:07:41 2009 +0000
@@ -6,6 +6,7 @@
 #
 # Authors:
 #      Mark Smith <mark@dreamwidth.org>
+#      Janine Costanzo <janine@netrophic.com>
 #
 # Copyright (c) 2009 by Dreamwidth Studios, LLC.
 #
@@ -21,44 +22,28 @@ body<=
     use vars qw/ %GET %POST $title /;
 
     # this page uses new style JS
-    LJ::need_res( 'stc/widget/shop.css' );
+    LJ::need_res( 'stc/shop.css' );
     LJ::set_active_resource_group( 'jquery' );
 
-    # build a cart
-    my $cart = DW::Shop->get->cart
-        or return 'Failed to get a shopping cart for you, please try again later.';
+    $title = $ML{'.title'};
 
-    # if they want us to remove...
-    my $cartid = $GET{cartid}+0;
-    my $itemid = $GET{itemid}+0;
-    my $action = $GET{action};
+    my $ret;
 
-    # remove the item then render the current cart
-    if ( $action eq 'remove' ) {
-        return 'Invalid cartid'
-            if $cart->id != $cartid;
-        return 'Failed to remove item'
-            unless $cart->remove_item( $itemid );
+    if ( LJ::did_post() ) {
+        my %from_post = LJ::Widget->handle_post( \%POST, ( 'ShopCart' ) );
+        $ret .= "<div class='shop-error'>$from_post{error}</div>"
+            if $from_post{error};
     }
 
-    # setup the output
-    my $ret = DW::Widget::ShopCartStatusBar->render( %GET );
+    $ret .= "<p><a href='$LJ::SITEROOT/shop'>&lt;&lt; $ML{'.backlink'}</a></p>";
 
-    # now render the contents of the cart
-    $ret .= '<ul>';
-    foreach my $item ( @{$cart->items} ) {
-        # FIXME: should require a POST to remove items
-        $ret .= '<li>[' . $item->id . ", <a href='$LJ::SITEROOT/shop/cart?cartid=" . $cart->id . "&itemid=";
-        $ret .= $item->id . "&action=remove'>remove</a>] ";
-        $ret .= $item->permanent ? '(permanent)' : ( '(' . $item->months . ' months)' );
-        $ret .= ' ' . $item->class . ' account for ' . $item->t_html;
-    }
-    $ret .= '</ul>';
+    $ret .= LJ::Widget::ShopCart->render;
 
-    # all done
+    $ret .= "<p><a href='$LJ::SITEROOT/shop'>&lt;&lt; $ML{'.backlink'}</a></p>";
+
     return $ret;
 }
 _code?>
 <=body
-title=>Your Shopping Cart
+title=><?_code return $title; _code?>
 page?>
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop/cart.bml.text
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/cart.bml.text	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,5 @@
+;; -*- coding: utf-8 -*-
+
+.backlink=Continue Shopping
+
+.title=Your Shopping Cart
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/shop/index.bml
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/index.bml	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,32 @@
+<?_c
+#
+# /shop/index.bml
+#
+# This is just a stub page that redirects to /shop.bml.
+#
+# Authors:
+#      Janine Costanzo <janine@netrophic.com>
+#
+# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+_c?><?page
+body<=
+<?_code
+{
+    use strict;
+    use vars qw/ %GET %POST $title $windowtitle $headextra @errors @warnings /;
+
+    return BML::redirect( "$LJ::SITEROOT/shop" );
+}
+_code?>
+<=body
+title=><?_code return $title; _code?>
+windowtitle=><?_code return $windowtitle; _code?>
+head<=
+<?_code return $headextra; _code?>
+<=head
+page?>
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/stc/shop.css
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/stc/shop.css	Tue Apr 14 08:07:41 2009 +0000
@@ -0,0 +1,113 @@
+/*
+    stc/shop.css
+   
+    CSS classes for rendering the various shop widgets and components.
+   
+    Authors:
+         Mark Smith <mark@dreamwidth.org>
+         Janine Costanzo <janine@netrophic.com>
+   
+    Copyright (c) 2009 by Dreamwidth Studios, LLC.
+   
+    This program is free software; you may redistribute it and/or modify it under
+    the same terms as Perl itself.  For a copy of the license, please reference
+    'perldoc perlartistic' or 'perldoc perlgpl'.
+*/
+
+
+.shopbox, .appwidget-shopitemgroupdisplay {
+    border: 1px solid #c1272d;
+    margin: 10px;
+    padding: 5px;
+    min-width: 30em;
+    max-width: 35em;
+    min-height: 20em;
+    float: left;
+}
+
+.leftybox {
+    max-width: 35em;
+    float: left;
+    margin: 10px;
+}
+
+.shop-account-status {
+    border: solid 1px #c1272d;
+    background-color: #ffff00;
+    float: right;
+    width: 30em;
+    padding: 5px;
+    margin: 10px;
+}
+
+.shop-table {
+    margin: 1em auto;
+    min-width: 50em;
+}
+
+.shop-table td {
+    vertical-align: top;
+    padding-left: 1em;
+}
+
+.shop-table-gift {
+    margin: 1em;
+}
+
+.shop-table-gift td {
+    vertical-align: top;
+    padding: 0.2em;
+}
+
+.shop-error {
+    border: 1px solid #c1272d;
+    clear: both;
+    padding: 5px;
+    margin: 10px;
+}
+
+.shop-cart-status {
+    border: 1px solid #c1272d;
+    background-color: #c5c5c5;
+    padding: 0.5em;
+}
+
+.shop-cart-status ul {
+    list-style: none;
+    margin: 0;
+    padding-bottom: 1.5em;
+}
+
+.shop-cart-status ul li {
+    float: left;
+    margin-right: 1em;
+}
+
+.shop-cart {
+    margin: 1em;
+}
+
+.shop-cart th {
+    background-color: #c5c5c5;
+}
+
+.shop-cart td, .shop-cart th {
+    border: 1px solid #c1272d;
+    padding: 0.5em;
+    text-align: center;
+}
+
+.shop-cart td.total {
+    font-weight: bold;
+    text-align: right;
+    background-color: #e0e0e0;
+}
+
+.shop-cart-btn {
+    margin-left: 1em;
+}
+
+.shop-item-highlight {
+    color: #c1272d;
+    font-weight: bold;
+}
diff -r 2530f9f773f5 -r b599f8330c87 htdocs/stc/widgets/shop.css
--- a/htdocs/stc/widgets/shop.css	Tue Apr 14 07:34:07 2009 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,66 +0,0 @@
-/*
-    stc/widgets/shop.css
-   
-    CSS classes for rendering the various shop widgets and components.
-   
-    Authors:
-         Mark Smith <mark@dreamwidth.org>
-   
-    Copyright (c) 2009 by Dreamwidth Studios, LLC.
-   
-    This program is free software; you may redistribute it and/or modify it under
-    the same terms as Perl itself.  For a copy of the license, please reference
-    'perldoc perlartistic' or 'perldoc perlgpl'.
-*/
-
-
-.shop-browse-separator {
-    padding-top: 10px;    
-}
-
-.shopbox {
-    border: solid 1px red;
-    margin: 10px;
-    padding: 5px;
-    max-width: 35em;
-    min-height: 20em;
-}
-
-.leftybox {
-    max-width: 35em;
-    float: left;
-    margin: 10px;
-}
-
-#left {
-    float: left;
-}
-
-#right {
-    float: right;
-}
-
-.shop-account-status {
-    border: solid 1px red;
-    background-color: yellow;
-    float: right;
-    width: 30em;
-    padding: 5px;
-    margin: 10px;
-}
-
-.shop-table {
-    margin: 1em auto;
-    min-width: 50em;
-}
-
-.shop-table td {
-    vertical-align: top;
-}
-
-.shop-error {
-    border: solid 1px red;
-    clear: both;
-    padding: 5px;
-    margin: 10px;
-}
--------------------------------------------------------------------------------