import gleam/dynamic/decode import gleam/javascript/promise import gleam/json import gleam/list import gleam/option.{type Option, None, Some} import storefront.{type ShopifyError, type StorefrontApiClientConfig, handler} import storefront/fragments.{cart_fragment} import storefront/utils.{ type Connection, type Cost, type Edge, type Image, type Money, type SelectedOption, Connection, Edge, cost_decoder, image_decoder, money_decoder, remove_edges_and_nodes, selected_option_decoder, } import wisp.{type Request} pub const cookie_name = "cartId" pub fn create_cart( config: StorefrontApiClientConfig, on_result: fn(Result(Cart, ShopifyError)) -> Nil, ) -> Nil { let handler = handler(config) handler.fetch( create_cart_mutation, None, shopify_create_cart_mutation_decoder(), ) |> promise.map(fn(res) { case res { Ok(cart) -> Ok(reshape_cart(cart.data.cart_create.cart)) Error(err) -> Error(err) } }) |> promise.tap(on_result) Nil } pub fn get_cart( config: StorefrontApiClientConfig, req: Request, on_result: fn(Result(Cart, ShopifyError)) -> Nil, ) -> Nil { let client = handler(config) case wisp.get_cookie(req, cookie_name, wisp.Signed) { Error(Nil) -> Nil Ok(id) -> { let query = get_cart_query let variables = json.object([#("cartId", json.string(id))]) let fetch = client.fetch(query, Some(variables), shopify_cart_operation_decoder()) promise.map_try(fetch, fn(res) { let cart = res.data.cart case cart { Some(cart) -> { let x = reshape_cart(cart) Ok(x) } None -> Error(storefront.NetworkError) } }) |> promise.tap(on_result) Nil } } } pub fn add_to_cart( config: StorefrontApiClientConfig, lines: List(CartItem), req: Request, on_result: fn(Result(Cart, ShopifyError)) -> Nil, ) -> Nil { let client = handler(config) case wisp.get_cookie(req, cookie_name, wisp.Signed) { Error(Nil) -> create_cart(config, on_result) Ok(id) -> { let query = add_to_cart_mutation let lines_json = lines |> list.map(fn(line) { shopify_add_to_cart_operation_variables_lines_to_json( ShopifyAddToCartOperationVariablesLines( merchandise_id: line.merchandise.id, quantity: line.quantity, ), ) }) let variables = json.object([ #("cartId", json.string(id)), #("lines", lines_json |> json.preprocessed_array), ]) let fetch = client.fetch( query, Some(variables), shopify_add_to_cart_operation_decoder(), ) promise.map_try(fetch, fn(res) { let cart = Ok(reshape_cart(res.data.cart)) cart }) |> promise.tap(on_result) Nil } } } pub fn remove_from_cart( config: StorefrontApiClientConfig, lines_ids: List(String), req: Request, on_result: fn(Result(Cart, ShopifyError)) -> Nil, ) -> Nil { let client = handler(config) case wisp.get_cookie(req, cookie_name, wisp.Signed) { Error(Nil) -> create_cart(config, on_result) Ok(id) -> { let query = add_to_cart_mutation let variables = json.object([ #("cartId", json.string(id)), #("lineIds", json.array(lines_ids, of: json.string)), ]) let fetch = client.fetch( query, Some(variables), shopify_remove_from_cart_operation_decoder(), ) promise.map_try(fetch, fn(res) { let cart = Ok(reshape_cart(res.data.cart_lines_remove.cart)) cart }) |> promise.tap(on_result) Nil } } } pub fn update_cart( config: StorefrontApiClientConfig, lines: List(CartItem), req: Request, on_result: fn(Result(Cart, ShopifyError)) -> Nil, ) { let client = handler(config) case wisp.get_cookie(req, cookie_name, wisp.Signed) { Error(Nil) -> create_cart(config, on_result) Ok(id) -> { let query = edit_cart_mutation let lines_json = { lines |> list.map(fn(line) { shopify_update_cart_line_update_to_json(ShopifyUpdateCartLineUpdate( id:, merchandise_id: line.merchandise.id, quantity: line.quantity, )) }) } let variables = json.object([ #("cartId", json.string(id)), #("lines", lines_json |> json.preprocessed_array), ]) let fetch = client.fetch( query, Some(variables), shopify_update_cart_operation_decoder(), ) promise.map_try(fetch, fn(res) { Ok(reshape_cart(res.data.cart_lines_update.cart)) }) |> promise.tap(on_result) Nil } } } pub fn reshape_cart(cart: ShopifyCart) -> Cart { let result = { case cart.cost { None -> None Some(cost) -> { let lines = remove_edges_and_nodes(cart.lines) Some(Cart( checkout_url: cart.checkout_url, id: cart.id, lines: lines, total_quantity: cart.total_quantity, cost: Some(ShopifyCartCost( subtotal_amount: cost.subtotal_amount, total_tax_amount: cost.total_tax_amount, total_amount: cost.total_amount, )), )) } } } let lines = remove_edges_and_nodes(cart.lines) option.unwrap( result, Cart( checkout_url: cart.checkout_url, id: cart.id, lines: lines, total_quantity: cart.total_quantity, cost: cart.cost, ), ) } pub const add_to_cart_mutation = " mutation addToCart($cartId: ID!, $lines: [CartLineInput!]!) { cartLinesAdd(cartId: $cartId, lines: $lines) { cart { ...cart } } } " <> cart_fragment pub opaque type CartLinesAdd { CartLinesAdd(cart: ShopifyCart) } fn cart_lines_add_decoder() -> decode.Decoder(CartLinesAdd) { use cart <- decode.field("cart", shopify_cart_decoder()) decode.success(CartLinesAdd(cart:)) } type ShopifyAddToCartOperation { ShopifyAddToCartMutation( data: CartLinesAdd, variables: List(ShopifyAddToCartOperationVariables), ) } fn shopify_add_to_cart_operation_decoder() -> decode.Decoder( ShopifyAddToCartOperation, ) { use data <- decode.field("data", cart_lines_add_decoder()) use variables <- decode.field( "variables", decode.list(shopify_add_to_cart_operation_variables_decoder()), ) decode.success(ShopifyAddToCartMutation(data:, variables:)) } fn shopify_add_to_cart_operation_variables_decoder() -> decode.Decoder( ShopifyAddToCartOperationVariables, ) { use cart_id <- decode.field("cart_id", decode.string) use lines <- decode.field( "lines", shopify_add_to_cart_operation_variables_lines_decoder(), ) decode.success(ShopifyAddToCartOperationVariables(cart_id:, lines:)) } type ShopifyAddToCartOperationVariablesLines { ShopifyAddToCartOperationVariablesLines(merchandise_id: String, quantity: Int) } fn shopify_add_to_cart_operation_variables_lines_to_json( shopify_add_to_cart_operation_variables_lines: ShopifyAddToCartOperationVariablesLines, ) -> json.Json { let ShopifyAddToCartOperationVariablesLines(merchandise_id:, quantity:) = shopify_add_to_cart_operation_variables_lines json.object([ #("merchandise_id", json.string(merchandise_id)), #("quantity", json.int(quantity)), ]) } fn shopify_add_to_cart_operation_variables_lines_decoder() -> decode.Decoder( ShopifyAddToCartOperationVariablesLines, ) { use merchandise_id <- decode.field("merchandise_id", decode.string) use quantity <- decode.field("quantity", decode.int) decode.success(ShopifyAddToCartOperationVariablesLines( merchandise_id:, quantity:, )) } type ShopifyAddToCartOperationVariables { ShopifyAddToCartOperationVariables( cart_id: String, lines: ShopifyAddToCartOperationVariablesLines, ) } pub const create_cart_mutation = " mutation createCart($lineItems: [CartLineInput!]) { cartCreate(input: { lines: $lineItems }) { cart { ...cart } } } " <> cart_fragment type ShopifyCreateCartMutation { ShopifyCreateCartMutation(data: ShopifyCreateCartData) } fn shopify_create_cart_mutation_decoder() -> decode.Decoder( ShopifyCreateCartMutation, ) { use data <- decode.field("data", shopify_create_cart_data_decoder()) decode.success(ShopifyCreateCartMutation(data:)) } type ShopifyCreateCartData { ShopifyCreateCartData(cart_create: CreateCart) } fn shopify_create_cart_data_decoder() -> decode.Decoder(ShopifyCreateCartData) { use cart_create <- decode.field("cartCreate", create_cart_decoder()) decode.success(ShopifyCreateCartData(cart_create:)) } type CreateCart { CreateCart(cart: ShopifyCart) } fn create_cart_decoder() -> decode.Decoder(CreateCart) { use cart <- decode.field("cart", shopify_cart_decoder()) decode.success(CreateCart(cart:)) } // REMOVE CART MUTATIONS AND DECODERS pub const remove_from_cart_mutation = " mutation removeFromCart($cartId: ID!, $lineIds: [ID!]!) { cartLinesRemove(cartId: $cartId, lineIds: $lineIds) { cart { ...cart } } } " <> cart_fragment type ShopifyRemoveFromCartOperation { ShopifyRemoveFromCartOperation( data: RemoveOperationData, variables: ShopifyRemoveFromCartVariables, ) } fn shopify_remove_from_cart_operation_decoder() -> decode.Decoder( ShopifyRemoveFromCartOperation, ) { use data <- decode.field("data", remove_operation_data_decoder()) use variables <- decode.field( "variables", shopify_remove_from_cart_variables_decoder(), ) decode.success(ShopifyRemoveFromCartOperation(data:, variables:)) } type ShopifyRemoveFromCartVariables { ShopifyRemoveFromCartVariables(cart_id: String, line_ids: List(String)) } fn shopify_remove_from_cart_variables_decoder() -> decode.Decoder( ShopifyRemoveFromCartVariables, ) { use cart_id <- decode.field("cartId", decode.string) use line_ids <- decode.field("lineIds", decode.list(decode.string)) decode.success(ShopifyRemoveFromCartVariables(cart_id:, line_ids:)) } type RemoveOperationData { RemoveOperationData(cart_lines_remove: CartLinesRemove) } fn remove_operation_data_decoder() -> decode.Decoder(RemoveOperationData) { use cart_lines_remove <- decode.field( "cartLinesRemove", cart_lines_remove_decoder(), ) decode.success(RemoveOperationData(cart_lines_remove:)) } type CartLinesRemove { CartLinesRemove(cart: ShopifyCart) } fn cart_lines_remove_decoder() -> decode.Decoder(CartLinesRemove) { use cart <- decode.field("cart", shopify_cart_decoder()) decode.success(CartLinesRemove(cart:)) } pub const edit_cart_mutation = " mutation editCartItems($cartId: ID!, $lines: [CartLineUpdateInput!]!) { cartLinesUpdate(cartId: $cartId, lines: $lines) { cart { ...cart } } } " <> cart_fragment type ShopifyUpdateCartOperation { ShopifyUpdateCartOperation( data: UpdateOperationData, variables: ShopifyUpdateCartVariables, ) } fn shopify_update_cart_operation_decoder() -> decode.Decoder( ShopifyUpdateCartOperation, ) { use data <- decode.field("data", update_operation_data_decoder()) use variables <- decode.field( "variables", shopify_update_cart_variables_decoder(), ) decode.success(ShopifyUpdateCartOperation(data:, variables:)) } type UpdateOperationData { UpdateOperationData(cart_lines_update: CartLinesUpdate) } fn update_operation_data_decoder() -> decode.Decoder(UpdateOperationData) { use cart_lines_update <- decode.field( "cartLinesUpdate", cart_lines_update_decoder(), ) decode.success(UpdateOperationData(cart_lines_update:)) } type CartLinesUpdate { CartLinesUpdate(cart: ShopifyCart) } fn cart_lines_update_decoder() -> decode.Decoder(CartLinesUpdate) { use cart <- decode.field("cart", shopify_cart_decoder()) decode.success(CartLinesUpdate(cart:)) } type ShopifyUpdateCartVariables { ShopifyUpdateCartVariables( cart_id: String, lines: List(ShopifyUpdateCartLineUpdate), ) } fn shopify_update_cart_variables_decoder() -> decode.Decoder( ShopifyUpdateCartVariables, ) { use cart_id <- decode.field("cartId", decode.string) use lines <- decode.field( "lines", decode.list(shopify_update_cart_line_update_decoder()), ) decode.success(ShopifyUpdateCartVariables(cart_id:, lines:)) } type ShopifyUpdateCartLineUpdate { ShopifyUpdateCartLineUpdate(id: String, merchandise_id: String, quantity: Int) } fn shopify_update_cart_line_update_to_json( shopify_update_cart_line_update: ShopifyUpdateCartLineUpdate, ) -> json.Json { let ShopifyUpdateCartLineUpdate(id:, merchandise_id:, quantity:) = shopify_update_cart_line_update json.object([ #("id", json.string(id)), #("merchandiseId", json.string(merchandise_id)), #("quantity", json.int(quantity)), ]) } fn shopify_update_cart_line_update_decoder() -> decode.Decoder( ShopifyUpdateCartLineUpdate, ) { use id <- decode.field("id", decode.string) use merchandise_id <- decode.field("merchandiseId", decode.string) use quantity <- decode.field("quantity", decode.int) decode.success(ShopifyUpdateCartLineUpdate(id:, merchandise_id:, quantity:)) } pub const get_cart_query = " query getCart($cartId: ID!) { cart(id: $cartId) { ...cart } } " <> cart_fragment pub opaque type ShopifyCartOperation { ShopifyCartOperation( data: ShopifyCartOperationData, variables: ShopifyCartOperationVariables, ) } fn shopify_cart_operation_decoder() -> decode.Decoder(ShopifyCartOperation) { use data <- decode.field("data", shopify_cart_operation_data_decoder()) use variables <- decode.field( "variables", shopify_cart_operation_variables_decoder(), ) decode.success(ShopifyCartOperation(data:, variables:)) } type ShopifyCartOperationData { ShopifyCartOperationData(cart: Option(ShopifyCart)) } fn shopify_cart_operation_data_decoder() -> decode.Decoder( ShopifyCartOperationData, ) { use cart <- decode.field("cart", decode.optional(shopify_cart_decoder())) decode.success(ShopifyCartOperationData(cart:)) } type ShopifyCartOperationVariables { ShopifyCartOperationVariables(cart_id: String) } fn shopify_cart_operation_variables_decoder() -> decode.Decoder( ShopifyCartOperationVariables, ) { use cart_id <- decode.field("cartId", decode.string) decode.success(ShopifyCartOperationVariables(cart_id:)) } pub type Cart { Cart( id: Option(String), checkout_url: String, cost: Option(ShopifyCartCost), total_quantity: Int, lines: List(CartItem), ) } pub fn cart_decoder() -> decode.Decoder(Cart) { use id <- decode.field("id", decode.optional(decode.string)) use checkout_url <- decode.field("checkoutUrl", decode.string) use cost <- decode.field("cost", decode.optional(shopify_cart_cost_decoder())) use total_quantity <- decode.field("totalQuantity", decode.int) use lines <- decode.field("lines", decode.list(cart_item_decoder())) decode.success(Cart(id:, checkout_url:, cost:, total_quantity:, lines:)) } pub type ShopifyCart { ShopifyCart( id: Option(String), checkout_url: String, cost: Option(ShopifyCartCost), lines: Connection(CartItem), total_quantity: Int, ) } fn cart_item_edge_decoder() -> decode.Decoder(Edge(CartItem)) { use node <- decode.field("node", cart_item_decoder()) decode.success(Edge(node:)) } fn cart_item_connection_decoder() -> decode.Decoder(Connection(CartItem)) { use edges <- decode.field("edges", decode.list(cart_item_edge_decoder())) decode.success(Connection(edges:)) } fn shopify_cart_decoder() -> decode.Decoder(ShopifyCart) { use id <- decode.field("id", decode.optional(decode.string)) use checkout_url <- decode.field("checkoutUrl", decode.string) use cost <- decode.field("cost", decode.optional(shopify_cart_cost_decoder())) use lines <- decode.field("lines", cart_item_connection_decoder()) use total_quantity <- decode.field("totalQuantity", decode.int) decode.success(ShopifyCart(id:, checkout_url:, cost:, lines:, total_quantity:)) } pub type ShopifyCartCost { ShopifyCartCost( total_amount: Money, subtotal_amount: Money, total_tax_amount: Money, ) } fn shopify_cart_cost_decoder() -> decode.Decoder(ShopifyCartCost) { use total_amount <- decode.field("totalAmount", money_decoder()) use subtotal_amount <- decode.field("subtotalAmount", money_decoder()) use total_tax_amount <- decode.field("totalTaxAmount", money_decoder()) decode.success(ShopifyCartCost( total_amount:, subtotal_amount:, total_tax_amount:, )) } pub type CartItem { CartItem( id: Option(String), quantity: Int, cost: Cost, merchandise: Merchandise, ) } fn cart_item_decoder() -> decode.Decoder(CartItem) { use id <- decode.field("id", decode.optional(decode.string)) use quantity <- decode.field("quantity", decode.int) use cost <- decode.field("cost", cost_decoder()) use merchandise <- decode.field("merchandise", merchandise_decoder()) decode.success(CartItem(id:, quantity:, cost:, merchandise:)) } pub type Merchandise { Merchandise( id: String, title: String, selected_options: List(SelectedOption), product: CartProduct, ) } pub fn merchandise_decoder() -> decode.Decoder(Merchandise) { use id <- decode.field("id", decode.string) use title <- decode.field("title", decode.string) use selected_options <- decode.field( "selectedOptions", decode.list(selected_option_decoder()), ) use product <- decode.field("product", cart_product_decoder()) decode.success(Merchandise(id:, title:, selected_options:, product:)) } pub type CartProduct { CartProduct(id: String, handle: String, title: String, featured_image: Image) } fn cart_product_decoder() -> decode.Decoder(CartProduct) { use id <- decode.field("id", decode.string) use handle <- decode.field("handle", decode.string) use title <- decode.field("title", decode.string) use featured_image <- decode.field("featuredImage", image_decoder()) decode.success(CartProduct(id:, handle:, title:, featured_image:)) }