storefront-qwik-starter icon indicating copy to clipboard operation
storefront-qwik-starter copied to clipboard

customer address is not updating

Open ashishkpaul opened this issue 11 months ago • 7 comments

Customer address is updating after refreshing the page

Screenshot_2024-12-24_12-58-25

ashishkpaul avatar Dec 24 '24 07:12 ashishkpaul

Hi, thanks for your feedback, is it something related to the latest update? ( Qwik v2 )

gioboa avatar Dec 24 '24 08:12 gioboa

same issue on version 1.11, commit console log output

be [Error]: CSRF check failed. Cross-site POST form submissions are forbidden.
The request origin "https://storefront.dingpack.com" does not match the server origin "http://storefront.dingpack.com".
    at Object.error (file:///mnt/ext-hdd/projects/vendure/storefront/fresh/storefront-qwik-starter/server/entry.express.js:15:8104)
    at jt (file:///mnt/ext-hdd/projects/vendure/storefront/fresh/storefront-qwik-starter/server/entry.express.js:17:1221)
    at AsyncLocalStorage.run (node:async_hooks:335:14)
    at Object.Me [as next] (file:///mnt/ext-hdd/projects/vendure/storefront/fresh/storefront-qwik-starter/server/entry.express.js:15:6930)
    at ce (file:///mnt/ext-hdd/projects/vendure/storefront/fresh/storefront-qwik-starter/server/entry.express.js:15:5531)
    at AsyncLocalStorage.run (node:async_hooks:346:14)
    at Rt (file:///mnt/ext-hdd/projects/vendure/storefront/fresh/storefront-qwik-starter/server/entry.express.js:15:5473)
    at Gt (file:///mnt/ext-hdd/projects/vendure/storefront/fresh/storefront-qwik-starter/server/entry.express.js:18:2375)
    at async router (file:///mnt/ext-hdd/projects/vendure/storefront/fresh/storefront-qwik-starter/server/entry.express.js:18:5930) {
  status: 403
}

ashishkpaul avatar Dec 24 '24 13:12 ashishkpaul

Cross-Origin issue, please ignore this

ashishkpaul avatar Dec 24 '24 13:12 ashishkpaul

Cross-Origin issue, please ignore this

Can we close this issue?

gioboa avatar Dec 24 '24 16:12 gioboa

Yes sir

On Tue, 24 Dec, 2024, 21:40 Giorgio Boa, @.***> wrote:

Cross-Origin issue, please ignore this

Can we close this issue?

— Reply to this email directly, view it on GitHub https://github.com/vendure-ecommerce/storefront-qwik-starter/issues/171#issuecomment-2561262394, or unsubscribe https://github.com/notifications/unsubscribe-auth/AD4NU72JUJZFIX3BOBEG4NT2HGBPLAVCNFSM6AAAAABUEJAVJ6VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDKNRRGI3DEMZZGQ . You are receiving this because you authored the thread.Message ID: @.*** com>

ashishkpaul avatar Dec 24 '24 16:12 ashishkpaul

Hi @gioboa , I rechecked, and the issue persists. api url https://core.vendure.lan, nginx conf file

# Upstream server for Vendure storefront
upstream storefront_lan {
    server localhost:3030;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name storefront.lan www.storefront.lan;
    return 301 https://www.storefront.lan$request_uri;
}

# HTTPS server
server {
    listen 443 ssl;
    server_name www.storefront.lan;

    # SSL certificate configuration
    ssl_certificate /etc/nginx/certs/nginx.crt;
    ssl_certificate_key /etc/nginx/certs/nginx.key;
    # Optional: Enable SSL ciphers and protocols
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-CBC-SHA256:ECDHE-ECDSA-AES128-CBC-SHA256:ECDHE-RSA-AES256-CBC-SHA:ECDHE-ECDSA-AES256-CBC-SHA:RSA-AES128-CBC-SHA256:RSA-AES256-CBC-SHA;  

    ssl_protocols TLSv1.2 TLSv1.3;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "same-origin" always;

    # CORS headers
    add_header 'Access-Control-Allow-Origin' 'https://www.storefront.lan' always;  # Adjust as needed
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, X-Requested-With, Accept' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;  # Required if using cookies or authentication

    # Handle preflight requests
    if ($request_method = OPTIONS) {
        return 204;
    }

    client_max_body_size 5M;

    location / {
        proxy_pass http://storefront_lan;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port 443;
    }
}

and vendure configuration file

apiOptions: {
        hostname: process.env.apiOptions_HOSTNAME ?? "192.168.1.32",
        port: serverPort,
        adminApiPath: 'admin-api',
        shopApiPath: 'shop-api',
        middleware: [
            {
                route: 'admin-api',
                handler: apiRateLimiter,
            },
            {
                route: 'shop-api',
                handler: apiRateLimiter,
            },
        ],
        cors: {
            origin: ['https://www.storefront.lan', 'https://core.vendure.lan'],
            credentials: true,
            methods: ['GET', 'POST', 'OPTIONS'],
            allowedHeaders: ['Content-Type', 'Authorization', 'vendure-token'],
        },
        introspection: IS_DEV ? true : false,
        ...(IS_DEV ? {
            adminApiPlayground: {
                settings: { 'request.credentials': 'include' },
            },
            adminApiDebug: true,
            shopApiPlayground: {
                settings: { 'request.credentials': 'include' },
            },
            shopApiDebug: true,
        } : {
            adminApiPlayground: false,
            adminApiDebug: false,
            shopApiPlayground: false,
            shopApiDebug: false,
        }),
    },

ashishkpaul avatar Feb 06 '25 09:02 ashishkpaul

Thanks for your update. I'll reopen it

gioboa avatar Feb 06 '25 12:02 gioboa

Analyzing Your Code

Looking at your src/routes/account/index.tsx file, within the updateCustomer function:

const updateCustomer = $(async (): Promise<void> => {
	await updateCustomerMutation(appState.customer);

	appState.customer.emailAddress !== newEmail.value
		? (showModal.value = true)
		: (isEditing.value = false);
});

And the useVisibleTask$ block where appState.customer is populated:

useVisibleTask$(async () => {
	const activeCustomer = await getActiveCustomerQuery();
	appState.customer = {
		title: activeCustomer.title ?? '',
		firstName: activeCustomer.firstName,
		id: activeCustomer.id,
		lastName: activeCustomer.lastName,
		emailAddress: activeCustomer.emailAddress,
		phoneNumber: activeCustomer.phoneNumber ?? '',
	};
	newEmail.value = activeCustomer?.emailAddress as string;
});

You are correctly fetching the activeCustomer data, which includes the id. However, when you call updateCustomerMutation(appState.customer), you are passing the entire appState.customer object as the input to the mutation. This object contains the id field, which is not allowed by the UpdateCustomerInput type in your GraphQL schema.

How to Fix It

You need to ensure that the object you pass to updateCustomerMutation only includes the fields defined in the UpdateCustomerInput type. You can do this by creating a new object with only the allowed fields when calling the mutation.

Modify your updateCustomer function in src/routes/account/index.tsx like this:

const updateCustomer = $(async (): Promise<void> => {
	const updateInput = {
		title: appState.customer.title,
		firstName: appState.customer.firstName,
		lastName: appState.customer.lastName,
		phoneNumber: appState.customer.phoneNumber,
		// customFields: appState.customer.customFields, // If you have customFields in your appState
	};
	await updateCustomerMutation(updateInput);

	appState.customer.emailAddress !== newEmail.value
		? (showModal.value = true)
		: (isEditing.value = false);
});

Complete Updated Snippet src/routes/account/index.tsx

import { $, component$, useContext, useSignal, useVisibleTask$ } from '@qwik.dev/core';
import { isBrowser } from '@qwik.dev/core/build';
import { Image } from 'qwik-image';
import { Button } from '~/components/buttons/Button';
import { HighlightedButton } from '~/components/buttons/HighlightedButton';
import { ErrorMessage } from '~/components/error-message/ErrorMessage';
import CheckIcon from '~/components/icons/CheckIcon';
import PencilSquareIcon from '~/components/icons/PencilSquareIcon';
import ShieldCheckIcon from '~/components/icons/ShieldCheckIcon';
import XMarkIcon from '~/components/icons/XMarkIcon';
import { Modal } from '~/components/modal/Modal';
import { APP_STATE } from '~/constants';
import {
	requestUpdateCustomerEmailAddressMutation,
	updateCustomerMutation,
} from '~/providers/shop/account/account';
import { getActiveCustomerQuery } from '~/providers/shop/customer/customer';
import { ActiveCustomer } from '~/types';

export default component$(() => {
	const appState = useContext(APP_STATE);
	const isEditing = useSignal(false);
	const showModal = useSignal(false);
	const newEmail = useSignal('');
	const errorMessage = useSignal('');
	const currentPassword = useSignal('');
	const update = {
		customer: {} as ActiveCustomer,
	};

	useVisibleTask$(async () => {
		const activeCustomer = await getActiveCustomerQuery();
		appState.customer = {
			title: activeCustomer.title ?? '',
			firstName: activeCustomer.firstName,
			id: activeCustomer.id,
			lastName: activeCustomer.lastName,
			emailAddress: activeCustomer.emailAddress,
			phoneNumber: activeCustomer.phoneNumber ?? '',
			// customFields: activeCustomer.customFields ?? {},
		};
		newEmail.value = activeCustomer?.emailAddress as string;
	});

	const updateCustomer = $(async (): Promise<void> => {
		const updateInput = {
			title: appState.customer.title,
			firstName: appState.customer.firstName,
			lastName: appState.customer.lastName,
			phoneNumber: appState.customer.phoneNumber,
			// customFields: appState.customer.customFields,
		};
		await updateCustomerMutation(updateInput);

		appState.customer.emailAddress !== newEmail.value
			? (showModal.value = true)
			: (isEditing.value = false);
	});

	const updateEmail = $(async (password: string, newEmail: string) => {
		const { requestUpdateCustomerEmailAddress } = await requestUpdateCustomerEmailAddressMutation(
			password,
			newEmail
		);
		if (requestUpdateCustomerEmailAddress.__typename === 'InvalidCredentialsError') {
			errorMessage.value = requestUpdateCustomerEmailAddress.message || '';
		} else {
			errorMessage.value = '';
			isEditing.value = false;
			showModal.value = false;
		}
	});

	return (
		<div>
			<div class="min-h-[24rem] max-w-6xl m-auto rounded-lg p-4 space-y-4 ">
				<div class="flex flex-col justify-center items-center">
					<div class="relative flex flex-col items-center rounded-[20px] w-[400px] mx-auto p-4 bg-white bg-clip-border shadow-xl hover:shadow-2xl">
						<div class="relative flex h-32 w-full justify-center rounded-xl bg-cover">
							<Image
								layout="fullWidth"
								src="/account-background.png"
								class="absolute flex h-32 w-full justify-center rounded-xl bg-cover"
								alt="background"
							/>
							<div class="absolute -bottom-12 flex h-[87px] w-[87px] items-center justify-center rounded-full border-[4px] border-white bg-pink-400">
								<Image
									layout="fullWidth"
									class="h-full w-full rounded-full"
									src="/user-icon.webp"
									alt="user icon"
								/>
							</div>
							<div class="absolute -bottom-12 right-0">
								<button
									class="hover:text-primary-700"
									onClick$={() => {
										isEditing.value = !isEditing.value;
										if (isBrowser) {
											window.scrollTo(0, 100);
										}
										if (!isEditing.value && isBrowser) {
											window.scrollTo(0, 0);
										}
									}}
								>
									<PencilSquareIcon />
								</button>
							</div>
						</div>
						<div class="mt-16 flex flex-col items-center pb-4">
							<h4 class="text-xl md:text-3xl font-bold">
								{appState.customer?.title && (
									<span class="text-base font-normal mr-1">{appState.customer?.title}</span>
								)}
								{appState.customer?.firstName} {appState.customer?.lastName}
							</h4>
						</div>
						<div class="flex flex-col items-center justify-center text-center">
							{appState.customer?.phoneNumber && (
								<div class="text-sm md:text-lg">
									Phone:
									<span class="font-bold px-2">{appState.customer?.phoneNumber}</span>
								</div>
							)}
							<div class="text-sm md:text-lg">
								Email:
								<span class="font-bold px-2">{appState.customer?.emailAddress}</span>
							</div>
						</div>
					</div>
				</div>
			</div>
			<div class="min-h-[24rem] rounded-lg p-4 space-y-4">
				<Modal
					open={showModal.value}
					title="Confirm E-Mail address change"
					onSubmit$={() => {
						updateEmail(currentPassword.value, newEmail.value);
					}}
					onCancel$={() => {
						showModal.value = false;
					}}
				>
					<div q:slot="modalIcon">
						<ShieldCheckIcon forcedClass="h-10 w-10 text-primary-500" />
					</div>
					<div q:slot="modalContent" class="space-y-4">
						<p>We will send a verification E-Mail to {newEmail.value}</p>

						<div class="space-y-1">
							<label html-for="password">Confirm the change by entering your password:</label>
							<input
								type="password"
								name="password"
								onChange$={(_, el) => {
									currentPassword.value = el.value;
								}}
								class="w-full"
							/>
						</div>

						{errorMessage.value !== '' && (
							<ErrorMessage
								heading="We ran into a problem changing your E-Mail!"
								message={errorMessage.value}
							/>
						)}
					</div>
				</Modal>
				{isEditing.value && (
					<div class="max-w-3xl m-auto">
						<div class="gap-4 grid grid-cols-1 md:grid-cols-2">
							<div class="md:col-span-2 md:w-1/4">
								<h3 class="text-sm text-gray-500">Title</h3>
								<input
									type="text"
									value={appState.customer?.title}
									onInput$={(_, el) => {
										update.customer.title = el.value;
									}}
									class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
								/>
							</div>

							<div>
								<label html-for="firstName" class="text-sm text-gray-500">
									First Name
								</label>
								<input
									type="text"
									value={appState.customer?.firstName}
									onChange$={(_, el) => {
										if (el.value !== '') {
											update.customer.firstName = el.value;
										}
									}}
									class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
								/>
							</div>
							<div>
								<label html-for="lastName" class="text-sm text-gray-500">
									Last Name
								</label>
								<input
									type="text"
									value={appState.customer?.lastName}
									onChange$={(_, el) => {
										if (el.value !== '') {
											update.customer.lastName = el.value;
										}
									}}
									class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
								/>
							</div>
							<div>
								<h3 class="text-sm text-gray-500">E-Mail</h3>
								<input
									type="email"
									value={appState.customer?.emailAddress}
									onChange$={(_, el) => {
										if (el.value !== '') {
											newEmail.value = el.value;
										}
									}}
									class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
								/>
							</div>

							<div>
								<h3 class="text-sm text-gray-500">Phone Nr.</h3>
								<input
									type="tel"
									value={appState.customer?.phoneNumber}
									onChange$={(_, el) => {
										update.customer.phoneNumber = el.value;
									}}
									class="block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
								/>
							</div>
						</div>

						<div class="flex gap-x-4 mt-8">
							<HighlightedButton
								onClick$={() => {
									appState.customer = { ...appState.customer, ...update.customer };
									updateCustomer();
								}}
							>
								<CheckIcon /> &nbsp; Save
							</HighlightedButton>

							<Button
								onClick$={() => {
									isEditing.value = false;
								}}
							>
								<XMarkIcon forcedClass="w-4 h-4" /> &nbsp; Cancel
							</Button>
						</div>
					</div>
				)}
			</div>
		</div>
	);
});

ashishkpaul avatar May 12 '25 06:05 ashishkpaul

Thanks @ashishkpaul for your help. Would you like to create a PR with this fix?

gioboa avatar May 12 '25 07:05 gioboa

Please do it from your end

ashishkpaul avatar May 12 '25 07:05 ashishkpaul

Thanks @ashishkpaul 👍 I did the fix, I added you as co-author 💯

gioboa avatar May 12 '25 07:05 gioboa