Skip to content

Commit

Permalink
Guest Checkout
Browse files Browse the repository at this point in the history
**Cart**

*The goal is a reactive offline / online multisession-multishop-multidevice cart.*

- if there is neither a sessionCart nor a userCart, create a sessionCart
  * add sessionId to cart.sessions
  * auto insert/update userId if authenticated
  * observers to ensure cart always exists (ie after deletion)
- if sessionCart is a not a userCart return sessionCart
- if sessionCart just authenticated, add userId
- only return a user cart when authenticated
  * copy existing userCart(s) into sessionCart and remove userCart(s)
- if userCart just logged out, remove sessionId from userCart and create new sessionCart
- allow multiple sessionId per cart when userCart

TODO:

- realistically this may not be very solid way to handle sessions.
- It will need a close review in the future.
- there are some edge cases where multiple authentications
or merges to the same cart may destroy the cart
- Cart really should have mirrored client and server methods
to allow adding to cart while offline
- allow multiple sessionId per cart when userCart
- consider implementation of this as part of Accounts

Updates issues:

- Remove packages introduced in reactioncommerce/reaction-core#76
- Resolves #271
- Resolves #339
- Resolves #326
- Resolves #183
- Strategic updates for issue #16
- Strategic updates for issue #318
- Strategic updates for issue #76

**Checkout**

   * dashboard enable/disable guest checkout
   * change default step to guest checkout or create/sign-in
   * store email for order - after order completion if guest
   * not visible after destroyed session (ie: refresh)
   * updated checkout and progress bar to use
     cart value instead of session

TODO:

- validation on email guest entry
- add account creation instructions to an order completion email
- potentially add third prompt for account creation after email (or promos,etc)
- order emails!
- use workflow states to control checkout ui flow.

**Accounts**

  * use new Accounts collection (previously referred to as customers)
  * userAccountsDropdown icon for orders, profile

  TODO:

- account profile / settings
- on account creation / login  with password:

    if confirmed account creation email
      create a account collection record with this userId
      update all orders with matching email to match this userId
      copy all order addresses into Accounts.profile.addressBook
      copy all social / email info to Accounts.
      users collection locked down, nothing exposed to client, used for authentication only
    else
      if there are more orders with this email
      display on order view "Add order"

**Packages**

* added settings.public to publish public settings to ReactionCore.xxx

**Orders**

* moves user orders from cart/checkout folder to dashboard/orders
* add message for confirmation of email
* authenticated user can see all orders where userId in list view
* admin can see all in list view
* userAccountsDropdown icon for orders
* add cartId to Orders (instead of using cartId as orderId)

TODO:

- integrate the admin view of list into dashboard admin flow
- this is possibly a breaking change to the orders dashboard.

**Dashboard**

* updates to handling settings from registry
* rename and move settingGeneral to shop/settings
* rename and move settingsAccounts to shop/accounts

**Multi-shop/vendor**

* shop account updates to prep multi-shop dashboard
* shopId added to cart.items (variants)

Strategic updates for issue #236
Strategic updates for issue #327

**General**

* Fixed footer layout pages loading.
* Updates CFS, removes FileStorage collection.

Related issues @aldeed @prinzdezibel
  • Loading branch information
aaronjudd committed Mar 16, 2015
1 parent bfdbfdd commit dd66c66
Show file tree
Hide file tree
Showing 91 changed files with 2,802 additions and 988 deletions.
28 changes: 17 additions & 11 deletions packages/reaction-core/client/app.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ _.extend ReactionCore,
isMember: false
isOwner: null
isAdmin: null
canCheckoutAsGuest: false
userPermissions: []
shopPermissions: []
shopPermissionGroups: []
Expand All @@ -19,24 +18,29 @@ _.extend ReactionCore,

if shop
self.shopId = shop._id
# check to see if guest checkout is enabled
self.canCheckoutAsGuest = shop.canCheckoutAsGuest || false
# permissions and packages
permissions = []

# get current enabled packages
self.usedPackages = ReactionCore.Collections.Packages.find({shopId: self.shopId, enabled: true}).fetch()
enabledPackages = ReactionCore.Collections.Packages.find(shopId: self.shopId, enabled: true).fetch()

# extract package registry permissions
for usedPackage in self.usedPackages
if usedPackage?.shopPermissions
for shopPermission in usedPackage.shopPermissions
for pkg in enabledPackages
if pkg?.shopPermissions
for shopPermission in pkg.shopPermissions
permissions.push shopPermission

self.shopPermissions = _.pluck(permissions, "permission")
self.shopPermissionGroups = for groupName, groupPermissions of _.groupBy(permissions, "group")
group: groupName
permissions: groupPermissions

# exposes public settings for packages
for pkg in enabledPackages
if pkg?.settings?.public
for setting, value of pkg.settings.public
ReactionCore[setting] = value

#XXX probably should use deps to recheck this whenever login/logout?
self.isOwner = Meteor.userId() is shop.ownerId

Expand All @@ -59,25 +63,27 @@ _.extend ReactionCore,
self.userPermissions = []
self.shopPermissions = []
self.shopPermissionGroups = []
# role checkout
hasOwnerAccess: ->
return Roles.userIsInRole(Meteor.user(), "admin") or @isOwner
# dashboard access
hasDashboardAccess: ->
return @isMember or @.hasOwnerAccess()

# permission check
hasPermission: (permissions) ->
return false unless permissions
permissions = [permissions] unless _.isArray(permissions)
return @.hasOwnerAccess() or _.intersection(permissions, @userPermissions).length or (@isAdmin and _.intersection(permissions, @shopPermissions).length)
# role checkout
hasOwnerAccess: ->
return Roles.userIsInRole(Meteor.user(), "admin") or @isOwner
# returns shop id
getShopId: ->
return @shopId


Meteor.startup ->
# todo: this could grow.. and grow...
# quick little client safety check
if (PackageRegistry?) then console.error "Bravely warning you that PackageRegistry should not be exported to client."

# Ignition.....
ReactionCore.init()
8 changes: 8 additions & 0 deletions packages/reaction-core/client/helpers/helpers.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,11 @@ currentProduct = @currentProduct
re = new RegExp("^6011")
return "discover" if number.match(re)?
""

###
# getGuestLoginState
# return true if guest checkout
# return userId if authenticated checkout
###
@getGuestLoginState = ->
Meteor.userId() || Session.equals "guestCheckoutFlow", true
6 changes: 2 additions & 4 deletions packages/reaction-core/client/helpers/spacebars.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,10 @@ Template.registerHelper "dateFormat", (context, block) ->
return context # moment plugin not available. return data as is.
return

Template.registerHelper "uc", (str) ->
encodeURIComponent str

###
# general helper for plurization of strings
# returns string with 's' concatenated if n = 1
# TODO: adapt to, and use i18n
###
Template.registerHelper "pluralize", (n, thing) ->
# fairly stupid pluralizer
Expand All @@ -224,7 +222,7 @@ Template.registerHelper "pluralize", (n, thing) ->

###
# general helper user name handling
# todo: needs additional validation all use cases
# TODO: needs additional validation all use cases
# returns first word in profile name
###
Template.registerHelper "fname", ->
Expand Down
22 changes: 17 additions & 5 deletions packages/reaction-core/client/subscriptions.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ ReactionCore.Subscriptions.Sessions = Meteor.subscribe "Sessions", amplify.store
Session.set "sessionId", serverSession._id
amplify.store "ReactionCore.session", serverSession._id

# subscribe to session dependant publications
ReactionCore.Subscriptions.cart = Meteor.subscribe "cart", serverSession._id, Meteor.userId()
ReactionCore.Subscriptions.account = Meteor.subscribe "accounts", serverSession._id, Meteor.userId()

# ensure cart resubscribed when removed
cart = ReactionCore.Collections.Cart.find('sessions': $in: [ serverSession._id ])
handle = cart.observeChanges(
removed: ->
#console.log "detected cart destruction... resetting now."
Meteor.subscribe "cart", serverSession._id, Meteor.userId()
return
)

###
# General Subscriptions
###
Expand All @@ -21,15 +34,14 @@ ReactionCore.Subscriptions.orders = Meteor.subscribe "orders"
ReactionCore.Subscriptions.customers = Meteor.subscribe "customers"
ReactionCore.Subscriptions.tags = Meteor.subscribe "tags"
ReactionCore.Subscriptions.media = Meteor.subscribe "media"
ReactionCore.Subscriptions.FileStorage = Meteor.subscribe "FileStorage"
ReactionCore.Subscriptions.cart = Meteor.subscribe "cart", Session.get "sessionId", Meteor.userId()

###
# Autorun dependencies
# ensure user cart is created, and address located
# Autorun dependencies
###
# account address initialization
Tracker.autorun ->
unless (Session.get('address') or Meteor.user()?.profile.addressBook)
account = ReactionCore.Collections.Accounts.findOne()
unless Session.get('address') or account?.profile?.addressBook
#Setting Default because we get here before location calc
address = {
latitude: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +0,0 @@
loginButtonsSession = Accounts._loginButtonsSession

Template._loginButtonsLoggedOutPasswordService.helpers
# Build a decorator to wrap the original function from Accounts package. When called,
# just modify the visible property of the password field.
fields: ((func) ->
->
fields = func()
unless loginButtonsSession.get("inSignupFlow")
fields.forEach (item, i) ->
if item.fieldName is "password"
item.visible = ->
not Session.get("Reactioncommerce.Core.loginButtons.inLoginAsGuestFlow")
return
return fields
)(Blaze._getTemplateHelper(Template._loginButtonsLoggedOutPasswordService, "fields"))

inLoginAsGuestFlow: ->
return Session.get "Reactioncommerce.Core.loginButtons.inLoginAsGuestFlow"

inLoginFlow: ->
return not Session.get("Reactioncommerce.Core.loginButtons.inLoginAsGuestFlow") and
not loginButtonsSession.get("inSignupFlow") and
not loginButtonsSession.get("inForgotPasswordFlow")

resetLoginFlow: ->
Session.set 'Reactioncommerce.Core.loginButtons.inLoginAsGuestFlow', false
loginButtonsSession.set "inSignupFlow", false
loginButtonsSession.set 'inForgotPasswordFlow', false

canCheckoutAsGuest: ->
!!ReactionCore.canCheckoutAsGuest
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,6 @@
<span data-i18n="accountsUI.signIn">Sign In</span>
{{/if}}
{{#if inline }}
{{#if canCheckoutAsGuest}}
{{#if inLoginAsGuestFlow}}
<span data-i18n="accountsUI.checkoutAsGuest">Checkout without user account</span>
{{/if}}
{{/if}}
{{/if}}
</div>

Expand All @@ -143,17 +138,6 @@
{{/if}}
{{#if inSignupFlow}}
{{> _loginButtonsBackToLoginLink}}
{{#if canCheckoutAsGuest }}
{{#if inline }}
{{> core_loginButtonsBackToGuestLoginLink}}
{{/if}}
{{/if}}
{{/if}}
{{#if canCheckoutAsGuest }}
{{#if inLoginAsGuestFlow}}
{{> _loginButtonsBackToLoginLink}}
{{> _loginButtonsBackToSignUpLink}}
{{/if}}
{{/if}}
</div>
{{/if}}
Expand Down
25 changes: 13 additions & 12 deletions packages/reaction-core/client/templates/accounts/accounts.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ Template.accounts.events
password = elementValueById("login-password")
loginButtonsSession.set "inSignupFlow", true
loginButtonsSession.set "inForgotPasswordFlow", false
Session.set "Reactioncommerce.Core.loginButtons.inLoginAsGuestFlow", false

# force the ui to update so that we have the approprate fields to fill in
Tracker.flush()
Expand Down Expand Up @@ -64,7 +63,6 @@ Template.accounts.events
usernameOrEmail = trimmedElementValueById("login-username-or-email")
loginButtonsSession.set "inSignupFlow", false
loginButtonsSession.set "inForgotPasswordFlow", true
Session.set "Reactioncommerce.Core.loginButtons.inLoginAsGuestFlow", false

# force the ui to update so that we have the approprate fields to fill in
Tracker.flush()
Expand All @@ -82,7 +80,6 @@ Template.accounts.events

loginButtonsSession.set 'inSignupFlow', false
loginButtonsSession.set 'inForgotPasswordFlow', false
Session.set "Reactioncommerce.Core.loginButtons.inLoginAsGuestFlow", true

# force the ui to update so that we have the approprate fields to fill in
Tracker.flush()
Expand All @@ -98,7 +95,6 @@ Template.accounts.events
password = elementValueById("login-password")
loginButtonsSession.set "inSignupFlow", false
loginButtonsSession.set "inForgotPasswordFlow", false
Session.set "Reactioncommerce.Core.loginButtons.inLoginAsGuestFlow", false

# force the ui to update so that we have the approprate fields to fill in
Tracker.flush()
Expand Down Expand Up @@ -161,11 +157,8 @@ trimmedElementValueById = (id) ->
element.value.replace /^\s*|\s*$/g, ""

loginOrSignup = ->

if loginButtonsSession.get("inSignupFlow")
signup()
else if Session.get('Reactioncommerce.Core.loginButtons.inLoginAsGuestFlow')
loginAsGuest()
else
login()
return
Expand Down Expand Up @@ -212,11 +205,12 @@ loginAsGuest = ->
if email isnt null
unless validateEmail(email)
return
Meteor.loginAsGuest email, (error, result) ->
if error
loginButtonsSession.errorMessage error
else
loginButtonsSession.closeDropdown()
CartWorkflow.loggedin()
# Meteor.loginAsGuest email, (error, result) ->
# if error
# loginButtonsSession.errorMessage error
# else
# loginButtonsSession.closeDropdown()
return

signup = ->
Expand Down Expand Up @@ -319,3 +313,10 @@ correctDropdownZIndexes = ->
n.style.zIndex = 1 if n.style.zIndex is 0
n = n.parentNode
return

###
# use all this with the unauthorized login as well
###

Template.unauthorized.inheritsHelpersFrom "accounts"
Template.unauthorized.inheritsEventsFrom "accounts"
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Template.loginDropdown.events
event.preventDefault()
template.$('.dropdown-toggle').dropdown('toggle') # close dropdown

"click .user-accounts-dropdown a": (event, template) ->
"click .user-accounts-dropdown-apps a": (event, template) ->
if @.route is "createProduct"
event.preventDefault()
Meteor.call "createProduct", (error, productId) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@

<template name="accountsDropdownApps">
<ul class="user-accounts-dropdown-apps">
{{> userAccountsDropdown}}
{{#if hasDashboardAccess}}
<!--administrative shortcut icons -->
{{#each reactionApps provides='shortcut' enabled=true}}
Expand All @@ -44,8 +45,6 @@
</li>
{{/each}}
{{> consoleIcon}}
{{else}}
{{> userAccountsDropdown}}
{{/if}}
</ul>
</template>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Template.loginInline.helpers
allowGuestCheckout: ->
return ReactionCore.allowGuestCheckout

Template.loginInline.events
'click .continue-guest': () ->
Session.set "guestCheckoutFlow", true
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
<template name="loginInline">
<div class="accounts-dialog accounts-inline">
{{>_loginButtonsLoggedOutAllServices inline="inline"}}
{{#if allowGuestCheckout}}
<div class="col-md-6 pull-left checkout-guest">
<div class="guest-checkout">
<p class="text-justify" data-i18n="checkoutLogin.guestMessage">Continue as a guest, and you can create an account later.
</p>
<div class="login-button single-login-button continue-guest">
<span data-i18n="checkoutLogin.continueAsGuest">Continue as Guest</span>
</div>
</div>
</div>
<div class="col-md-6 pull-right checkout-login">
{{>_loginButtonsLoggedOutAllServices inline="inline"}}
</div>
{{else}}
<div class="checkout-login">
{{>_loginButtonsLoggedOutAllServices inline="inline"}}
</div>
{{/if}}
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template name="accountProfile">
<div class="row">
<h3>Hello!</h3>
</div>
<div class="row">
{{> dashboardOrdersList}}
</div>
</template>
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
Media = ReactionCore.Collections.Media

Template.cartDrawerItems.rendered = ->
$ ->
mySwiper = $(".cart-drawer-swiper-container").swiper(
Expand All @@ -17,14 +15,14 @@ Template.cartDrawerItems.rendered = ->
Template.cartDrawerItems.helpers
media: ->
# return default image for this product variant
if defaultImage = Media.findOne({'metadata.variantId': @variants._id})
if defaultImage = ReactionCore.Collections.Media.findOne({'metadata.variantId': @variants._id})
return defaultImage
else
# loop through all product variants attempting to find default image
product = Products.findOne @productId
return unless product
img = null
_.any product.variants, (v) ->
img = Media.findOne({'metadata.variantId': v._id})
img = ReactionCore.Collections.Media.findOne({'metadata.variantId': v._id})
return !!img
return img
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Template.cartIcon.helpers
cart: ->
return Cart.findOne()
return ReactionCore.Collections.Cart.findOne()

Template.cartIcon.events
'click .cart-icon': () ->
Expand Down
Loading

0 comments on commit dd66c66

Please sign in to comment.