I created a custom gift box creator for a client. The current approach is that I created a "Gift Box" product and add the selected items as attributes on the cart item. This is all done in the theme's JS code but the problem I'm facing is that because I'm not actually adding the underlying items to the cart inventory is not updating.
Is there a way to not show the underlying items in the cart but have checkout update their inventory counts?
What if you had a microservice which responded to order/created and edited the order to include the items # $0?
You could use the steps outlined in this official Shopify tutorial on their GraphQL feature, and implemented in node:
import Shopify from 'shopify-api-node'; // https://github.com/MONEI/Shopify-api-node
shopify = new Shopify({/* your app credentials */});
// assume this is the HTTP endpoint w/ a JSON middleware
function(req, res) {
const order = req.body;
const giftBasket = order.line_items.find(li => li.id === 1234); // 1234, or whatever the product ID of your gift basket is.
const realItemSKUs = giftBasket.properties.filter((lineItemProp) => {
// assuming you use a product1: sku, product2: sku line item attribute name/value.
// but adjust to your needs.
return lineItemProp.name.match('product\d');
}).map((lineItemProp) => lineItemProp.value);
function createVariantQuery(sku) {
return `product${id}: {
productVariants(query: "sku:${sku}") { id }
}`;
}
const query = realItemSKUs.map((sku) => createVariantQuery(sku)).join('\n')
const variantIds = await shopify.graphql(`{${query}}`)
.then((variants) => Object.values(variants).map((variant) => variant.id));
const orderEditRes = await shopify.graphql(`mutation beginEdit{
orderEditBegin(id: "gid://shopify/Order/1234"){
calculatedOrder{
id
}
}
}`);
const calcLineItems = await Promise.all(variantIds.map(async (id) =>
shopify.graphql(`mutation addVariantToOrder{
orderEditAddVariant(id: "gid://shopify/CalculatedOrder/${orderEditRes.calculatedOrder}", variantId: "${id}", quantity: 1) {
calculatedOrder {
id
addedLineItems(first:5) {
edges {
node {
id
quantity
}
}
}
}
userErrors {
field
message
}
}
}`);
));
await Promise.all(calcLineItems.map(async (calcLineItem =>
shopify.graphql(`addDiscount {
orderEditAddLineItemDiscount(id: "${calculatedOrder.id}", lineItemId: "${calcLineItem.id}", discount: {percentValue: 100, description: "Giftbasket"}) {
calculatedOrder {
id
addedLineItems(first:5) {
edges {
node {
id
quantity
}
}
}
}
userErrors {
message
}
}
}`
);
return shopify.graphql(`mutation commitEdit {
orderEditCommit(id: "${calculatedOrder}", notifyCustomer: false, staffNote: "Auto giftbasket updated") {
order {
id
}
userErrors {
field
message
}
}
}`);
};
Related
I am working on a Shopware 6 Administrative plugin but displaying product images has been a big headache. I went through shopware repositories of 'product_media', 'media', and generally all folder repositories related to media.
I have not been able to decipher how image linking works since I can not get hold of the exact folder names.
How then do I go about this. Note: I have been able to get correct image names and relevant image data like id, folder id etc.
Below is my module/component idex.js file codes
import template from './images-page.html.twig';
const { Component, Context } = Shopware;
const { Criteria } = Shopware.Data;
Component.register('images', {
template,
inject: ['repositoryFactory', 'mediaService', 'acl'],
metaInfo() {
return {
title: 'images'
};
},
computed: {
/**productMediaRepository() {
return this.repositoryFactory.create(this.product.media.entity);
},*/
productRepository() {
return this.repositoryFactory.create('product');
},
mediaFolderRepository() {
return this.repositoryFactory.create('media_folder');
},
mediaRepository() {
return this.repositoryFactory.create('media');
},
rootFolder() {
const root = this.mediaFolderRepository.create(Context.api);
root.name = this.$tc('sw-media.index.rootFolderName');
root.id = null;
return root;
},
logRep(){
console.log(this.productRepository);
// console.log(this.mediaFolderRepository);
// console.log(this.mediaRepository);
// console.log(this.rootFolder);
}
},
methods: {
logProducts(){
const criteria = new Criteria();
this.productRepository
.search(criteria, Shopware.Context.api)
.then(result => {
console.log(result[0]);
});
},
logMediaFolders(){
const criteria = new Criteria();
this.mediaFolderRepository
.search(criteria, Shopware.Context.api)
.then(result => {
console.log(result);
});
}
},
created(){
this.logMediaFolders();
}
});
here's the twig template (nothing really here)
<sw-card title="Watermark">
<img src="" alt="Product Image" />
</sw-card>
The media elements of the products are associations that are not loaded automatically, but you can configure the criteria, so that those associations will be loaded directly when you fetch the products. Refer to the official docs for detailed infos.
In you case that means to load the cover image / or all product images, you would have to adjust the criteria you use for fetching the products the following way
logProducts(){
const criteria = new Criteria();
criteria.addAssociation('cover.media');
criteria.addAssociation('media.media');
this.productRepository
.search(criteria, Shopware.Context.api)
.then(result => {
console.log(result[0]);
});
},
Then to link to the cover image you can use:
<sw-card title="Watermark">
<img src="product.cover.media.url" alt="Product Image" />
</sw-card>
You find all the media elements of the product as an array under product.media and can also use product.media[0].media.url to link to those images.
In a Vue component controlling users subsciption to newsletters, I have the fellowing code:
async newSubscriber(event) {
// Validate email
//---------------
if (!this.isEmailValid(this.subscriber_email))
this.subscribeResult = "Email not valid";
else {
// If valid, check if email is not already recorded
//-------------------------------------------------
let alreadyRecorded = false;
let recordedEmails = await this.$apollo.query({ query: gql`query { newslettersEmails { email } }` });
console.log('length ' + recordedEmails.data.newslettersEmails.length);
console.log(recordedEmails.data.newslettersEmails);
for (let i = 0; !alreadyRecorded && i < recordedEmails.data.newslettersEmails.length; i++)
alreadyRecorded = this.subscriber_email === recordedEmails.data.newslettersEmails[i].email;
if (alreadyRecorded)
this.subscribeResult = "Email already recorded";
else {
// If not, record it and warn the user
//------------------------------------
this.$apollo.mutate({
mutation: gql`mutation ($subscriber_email: String!){
createNewslettersEmail(input: { data: { email: $subscriber_email } }) {
newslettersEmail {
email
}
}
}`,
variables: {
subscriber_email: this.subscriber_email,
}
})
.then((data) => { this.subscribeResult = "Email recorded"; })
.catch((error) => { this.subscribeResult = "Error recording the email: " + error.graphQLErrors[0].message; });
}
}
}
At the very first email subscription test, $apollo.query returns me the correct number of emails already recorded (let's say, 10) and record the new subscriber email. But if I try to record a second email without hard refreshing (F5) the browser, $apollo.query returns me the exact same result than the first time (10), EVEN IF the first test email has been correctly recorded by strapi (graphql palyground showns me the added email with the very same query!). Even if I add ten emails, apollo will always return me what it got during its first call (10 recorded emails), as if it uses a buffered result. Of course, that allows Vue to record several times the same email, which I obviously want to avoid!
Does it speaks to anyone ?
After a lot of Google digging (giving the desired results by simply changing in my requests, at the end, "buffering" by "caching" !), I understood that Apollo cache its queries by default (at least, in the configuration of the Vue project I received). To solve the problem I just added "fetchPolicy: 'network-only'" to the query I make:
let recordedEmails = await this.$apollo.query({
query: gql`query { newslettersEmails { email } }`,
});
became
let recordedEmails = await this.$apollo.query({
query: gql`query { newslettersEmails { email } }`,
fetchPolicy: 'network-only'
});
And problem solved ^^
I want to save a card for next payments in my app, but always get the same exception : "Stripe.StripeException: 'The provided PaymentMethod was previously used with a PaymentIntent without Customer attachment, shared with a connected account without Customer attachment, or was detached from a Customer. It may not be used again. To use a PaymentMethod multiple times, you must attach it to a Customer first.'
"
I don't have any clue, how to solve it.
Here is my Controller.cs:
public class PaymentController : Controller
{
public IActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Processing()
{
var service = new PaymentMethodService();
var obj=service.Get("pm_1ICLE7GcqJgpxMZpnTbfS7Jw");
var paymentIntents = new PaymentIntentService();
var paymentIntent = paymentIntents.Create(new PaymentIntentCreateOptions
{
Amount = 2000,
Currency = "usd",
Customer = "cus_Ing6wBxNYVdB44",
ReceiptEmail = "eman29#jdecorz.com",
PaymentMethod = obj.Id,
Confirm = true,
OffSession = true
});//here exception is thrown
return Json(new { clientSecret = paymentIntent.ClientSecret });
}
}
My client.js code:
var stripe = Stripe("pk_test_51IBEAOGcqJgpxMZpvVKN2j9K7RJpzazfnG4u0relgSXiVBtNDd7nGgxBmX8BNCvuNerv1jnf0UVL5Uz8ODeJ7wvI00ruu2ByVM");
// Disable the button until we have Stripe set up on the page
document.querySelector("button").disabled = true;
fetch("/Payment/Processing", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(purchase)
})
.then(function (result) {
return result.json();
})
.then(function (data) {
var elements = stripe.elements();
var style = {
base: {
color: "#32325d",
fontFamily: 'Arial, sans-serif',
fontSmoothing: "antialiased",
fontSize: "16px",
"::placeholder": {
color: "#32325d"
}
},
invalid: {
fontFamily: 'Arial, sans-serif',
color: "#fa755a",
iconColor: "#fa755a"
}
};
var card = elements.create("card", { style: style });
// Stripe injects an iframe into the DOM
card.mount("#card-element");
card.on("change", function (event) {
// Disable the Pay button if there are no card details in the Element
document.querySelector("button").disabled = event.empty;
document.querySelector("#card-error").textContent = event.error ? event.error.message : "";
});
var form = document.getElementById("payment-form");
form.addEventListener("submit", function (event) {
event.preventDefault();
// Complete payment when the submit button is clicked
payWithCard(stripe, card, data.clientSecret);
});
});
// Calls stripe.confirmCardPayment
// If the card requires authentication Stripe shows a pop-up modal to
// prompt the user to enter authentication details without leaving your page.
var payWithCard = function (stripe, card, clientSecret) {
loading(true);
stripe
.confirmCardPayment(clientSecret, {
payment_method: {
card: card
}
})
.then(function (result) {
if (result.error) {
// Show error to your customer
showError(result.error.message);
} else {
// The payment succeeded!
orderComplete(result.paymentIntent.id);
}
});
};
/* ------- UI helpers ------- */
// Shows a success message when the payment is complete
var orderComplete = function (paymentIntentId) {
loading(false);
document
.querySelector(".result-message a")
.setAttribute(
"href",
"https://dashboard.stripe.com/test/payments/" + paymentIntentId
);
document.querySelector(".result-message").classList.remove("hidden");
document.querySelector("button").disabled = true;
};
// Show the customer the error from Stripe if their card fails to charge
var showError = function (errorMsgText) {
loading(false);
var errorMsg = document.querySelector("#card-error");
errorMsg.textContent = errorMsgText;
setTimeout(function () {
errorMsg.textContent = "";
}, 4000);
};
// Show a spinner on payment submission
var loading = function (isLoading) {
if (isLoading) {
// Disable the button and show a spinner
document.querySelector("button").disabled = true;
document.querySelector("#spinner").classList.remove("hidden");
document.querySelector("#button-text").classList.add("hidden");
} else {
document.querySelector("button").disabled = false;
document.querySelector("#spinner").classList.add("hidden");
document.querySelector("#button-text").classList.remove("hidden");
}
};
I try to use samples from https://stripe.com/docs/payments/save-during-payment and https://stripe.com/docs/payments/save-and-reuse, but can't understand, what I do wrongly
I know this is the dumb question, but it is my first expierence with Stripe and I can't to find any solution for this problem.
Thank you in advance!
When re-using a PaymentMethod for a Customer, it must be attached to them. There is a few ways to go about that. For example, one option is to create a payment method and then to call attach in the backend [1]. The other option is to collect card information using Stripe.js and Elements and to "setup future usage", this will automatically attach the card to the customer [2].
One thing to note, if your code uses confirmCardPayment() [3], that would normally be an "on-session" payment as the user is actively confirming the charge. [4]
[1] https://stripe.com/docs/api/payment_methods/attach
[2] https://stripe.com/docs/js/payment_intents/confirm_card_payment#stripe_confirm_card_payment-data-setup_future_usage
[3] https://stripe.com/docs/js/payment_intents/confirm_card_payment
[4] https://stripe.com/docs/api/payment_intents/create#create_payment_intent-off_session
If an already added product is again added to the cart then only cart number should increase but the function appends the data to the list and the product gets duplicate in the cart view. How to make sure check if the product already exists then only increment or decrement the count.
below is the code to update product
const initialState = {
cart: [],
total: 0,
}
const cartItems = (state = initialState, action) => {
switch(action.type) {
case 'ADD_TO_CART':
return {
...state,
cart: [action.payload, ...state.cart],
total: state.total + 1
}
// return [...state,action.payload]
case 'REMOVE_FROM_CART' :
return state.filter(cartItems => cartItems.id !== action.payload.id)
}
return state
}
A single item from the data will be like
{key:'1', type:"EARRINGS", pic:require('../../assets/earring.jpg'),price:"200"}
If you are using the same key for the items you can do like below
case 'ADD_TO_CART':
{
const updatedCart = [...state.cart];
const item = updatedCart.find(x=>x.key===action.payload.key);
if(item)
{
item.count++;
}
else{
updatedCart.push(action.payload);
}
return {
...state,
cart: updatedCart,
total: state.total + 1
}
}
The logic would search for items in the array and increase the count or add a new item to the array.
I think this will work.
case UPDATE_CART:
let receivedItem = action.payload
let itemList = state.cart
let stepToUpdate = itemList.findIndex(el => el.id === receivedItem.id);
itemList[stepToUpdate] = { ... itemList[stepToUpdate], key: receivedItem };
return { ...state, cart: itemList }
'id' is a unique thing to update specific item present in your cart. It cab be product id or some other id.
itemList.findIndex(el => el.id === receivedItem.id);
There are different ways of achieving this. You can create actions to INCREMENT/DECREMENT in case you know the product is added (eg: on the cart summary).
And you can also let this behaviour inside the ADD_TO_CART action if you don't know whether the product is added or not:
case "ADD_TO_CART": {
const isProductAdded = state.cart.find(
item => action.payload.id === item.id
);
return {
...state,
cart: isProductAdded
? state.cart.map(item => {
if (item.id === action.payload.id) {
item.qty++;
}
return item;
})
: [action.payload, ...state.cart],
total: state.total + 1
};
}
In my application I have two separate components:
products component (show the products in the ui)
filter-products component (show the criteria in ui)
total-filter-products component (show the total criteria items was applied in ui, for example my current search is age,name so on)
They are separate and not communicate with input (:) or output (#).
I also have a ts file, with function useProducts as "api-composition style".
export const useProducts = () => {
const criteria = ref({ category: null, age: null, color: null });
const items = computed(() => { return store.get('products').filter(p => filterByCritiria(p)) });
const load = () => {
const params = parseSome(criteria.value);
axios.post('/api/...', { params }).then(r => { store.set('products', r.data)});
}
return { criteria, items, load }
}
In the products component, I just get the products from useProducts and bind to the template:
setup() {
const { products } = useProducts();
return { products }
}
In the filter-products, I get the criteria from useProducts and bind to the template. every time some value change in criteria ref (using v-model), I run the load function.
setup() {
const { criteria, load } = useProducts();
watch(() => criteria.value, (v) => load() });
return { criteria }
}
In total-filter-products component I do some logic to display the criteria:
setup() {
const { criteria } = useProducts();
return { criteria: parse(criteria) }
}
The problem with this is every time I call to useProducts I execute the function and get new variables.
I get three times the criteria. so if I change the criteria in filter-products, the total-filter-products and products components will never know about it.
How to share the composition across components in api-composition style?