Skip to content

Commit

Permalink
feat: newsletter confirmation (#72)
Browse files Browse the repository at this point in the history
* feat: newsletter confirmation (GH-65)

* feat: newsletter confirmation (GH-65)

* feat: newsletter confirmation (GH-65)

---------

Co-authored-by: Maciej Daniłowicz <[email protected]>
  • Loading branch information
patzick and mdanilowicz authored Mar 27, 2023
1 parent cbe16cc commit e13d3d9
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/curvy-rockets-smash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vue-demo-store": minor
---

Add a confirmation instruction box to the newsletter subscription panel
5 changes: 5 additions & 0 deletions .changeset/three-ads-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopware-pwa/api-client": minor
---

Add newsletter confirmation endpoint
1 change: 1 addition & 0 deletions packages/api-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from "./services/formsService";
export * from "./services/wishlistService";
export * from "./services/documentService";
export * from "./services/orderService";
export * from "./services/newsletterService";
export * from "./endpoints";

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { newsletterConfirmation } from "../newsletterService";
import { defaultInstance } from "../../apiService";
import { describe, expect, it, beforeEach, vi } from "vitest";

vi.mock("../../../src/apiService");
const mockedApiInstance = defaultInstance;

describe("NewsletterService - newsletterConfirmation", () => {
const mockedPost = vi.fn();
beforeEach(() => {
vi.resetAllMocks();
mockedApiInstance.invoke = {
post: mockedPost,
} as any;
});

it("should confirm newsletter", async () => {
mockedPost.mockResolvedValueOnce({ data: { data: {} } });
const result = await newsletterConfirmation({
hash: "232323",
em: "44242456",
});
expect(mockedPost).toBeCalledTimes(1);
expect(mockedPost).toBeCalledWith(`/store-api/newsletter/confirm`, {
hash: "232323",
em: "44242456",
});
expect(result).toMatchObject({});
});
});
2 changes: 1 addition & 1 deletion packages/api-client/src/services/formsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export async function newsletterSubscribe(
}

/**
* Unsuscribes from newsletter
* Unsubscribe from newsletter
*
* @param {NewsletterSubscribeData} params newsletter subscribe data: email
*
Expand Down
25 changes: 25 additions & 0 deletions packages/api-client/src/services/newsletterService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { getStoreNewsletterConfirmEndpoint } from "../endpoints";
import { defaultInstance, ShopwareApiInstance } from "../apiService";

type NewsletterConfirmationParams = {
em: string;
hash: string;
};

/**
* Confirmation from newsletter
*
* @param {NewsletterConfirmationParams} params
* @param {ShopwareApiInstance} contextInstance
* @returns
*/
export async function newsletterConfirmation(
params: NewsletterConfirmationParams,
contextInstance: ShopwareApiInstance = defaultInstance
): Promise<void> {
const response = await contextInstance.invoke.post(
getStoreNewsletterConfirmEndpoint(),
params
);
return response.data;
}
2 changes: 1 addition & 1 deletion packages/composables/src/helpers/deepMerge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function isObject(object: any): boolean {
* @returns merged object
*/
export default function deepMerge(obj1: any, obj2: any): object {
let output = Object.assign({}, obj1);
const output = Object.assign({}, obj1);
if (isObject(obj1) && isObject(obj2)) {
Object.keys(obj2).forEach((key) => {
if (isObject(obj2[key])) {
Expand Down
50 changes: 46 additions & 4 deletions packages/composables/src/useNewsletter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "@shopware-pwa/api-client";
import { NewsletterInput } from "@shopware-pwa/types";
import { useShopwareContext, useInternationalization } from ".";
import { ref, computed, ComputedRef, Ref } from "vue";

export type UseNewsletterReturn = {
/**
Expand All @@ -17,14 +18,34 @@ export type UseNewsletterReturn = {
* @param email
*/
newsletterUnsubscribe(email: string): Promise<void>;
/**
* Get newsletter status from the API call
*/
getNewsletterStatus(): Promise<void>;
/**
* Indicates if the user is subscribed to the newsletter
*
* Returns `true` if the user is subscribed to the newsletter, `false` otherwise
*/
isNewsletterSubscriber(): Promise<any>;
isNewsletterSubscriber: ComputedRef<boolean>;
/**
* Newsletter status
*/
newsletterStatus: Ref<NewsletterStatus>;
/**
* Inform about newsletter confirmation
*/
confirmationNeeded: ComputedRef<boolean>;
};

const enum NewsletterStatus {
NOT_SET = "notSet",
DIRECT = "direct",
UNDEFINED = "undefined",
OPT_OUT = "optOut",
OPT_IN = "optIn",
}

/**
* Composable for newsletter subscription.
* @public
Expand All @@ -33,6 +54,9 @@ export type UseNewsletterReturn = {
export function useNewsletter(): UseNewsletterReturn {
const { apiInstance } = useShopwareContext();
const { getStorefrontUrl } = useInternationalization();
const newsletterStatus: Ref<NewsletterStatus> = ref(
NewsletterStatus.UNDEFINED
);

async function newsletterSubscribe(params: NewsletterInput) {
return await newsletterSubscribeAPI(
Expand All @@ -53,14 +77,32 @@ export function useNewsletter(): UseNewsletterReturn {
);
}

async function isNewsletterSubscriber() {
const response = await isNewsletterSubscriberAPI(apiInstance);
return response.status !== "optOut";
async function getNewsletterStatus() {
try {
const response = await isNewsletterSubscriberAPI(apiInstance);
newsletterStatus.value = response.status as NewsletterStatus;
} catch (error) {
console.error(error);
}
}

const isNewsletterSubscriber = computed(
() =>
![NewsletterStatus.OPT_OUT, NewsletterStatus.UNDEFINED].includes(
newsletterStatus.value
)
);

const confirmationNeeded = computed(
() => newsletterStatus.value === NewsletterStatus.NOT_SET
);

return {
newsletterSubscribe,
newsletterUnsubscribe,
isNewsletterSubscriber,
getNewsletterStatus,
newsletterStatus,
confirmationNeeded,
};
}
35 changes: 26 additions & 9 deletions templates/vue-demo-store/pages/account/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ const {
userDefaultBillingAddress,
userDefaultShippingAddress,
} = useUser();
const { isNewsletterSubscriber, newsletterSubscribe } = useNewsletter();
const {
isNewsletterSubscriber,
newsletterUnsubscribe,
newsletterSubscribe,
getNewsletterStatus,
newsletterStatus,
confirmationNeeded,
} = useNewsletter();
const { pushSuccess, pushError } = useNotifications();
useBreadcrumbs([
Expand All @@ -29,8 +36,6 @@ useBreadcrumbs([
},
]);
newsletter.value = await isNewsletterSubscriber();
const updateNewsletterStatus = async () => {
try {
if (!newsletter.value) {
Expand All @@ -40,20 +45,23 @@ const updateNewsletterStatus = async () => {
});
pushSuccess("Newsletter subscribed");
} else {
await newsletterSubscribe({
email: user.value?.email || "",
option: "unsubscribe",
});
await newsletterUnsubscribe(user.value?.email || "");
pushSuccess("Newsletter unsubscribe");
}
} catch (error) {
newsletter.value = !newsletter.value;
console.log("error", error);
pushError("Something goes wrong please try again later");
} finally {
getNewsletterStatus().then(() => {
newsletter.value = isNewsletterSubscriber.value;
});
}
};
onBeforeMount(async () => {
getNewsletterStatus().then(() => {
newsletter.value = isNewsletterSubscriber.value;
});
if (user?.value?.salutationId) {
await loadSalutation(user.value.salutationId);
}
Expand Down Expand Up @@ -103,6 +111,16 @@ onBeforeMount(async () => {
</section>
<section class="mb-10">
<h3 class="border-b pb-3 font-bold mb-5">Newsletter setting</h3>
<div
v-if="confirmationNeeded"
class="bg-green-100 border-t border-b border-green-500 text-green-700 px-4 py-3 mb-4"
>
<p class="text-sm">
You have just subscribed to our newsletter. To complete the sign-up
process, search your inbox for our confirmation email and click on the
link provided with it.
</p>
</div>
<div class="flex">
<input
id="newsletter-checkbox"
Expand All @@ -112,7 +130,6 @@ onBeforeMount(async () => {
class="h-4 w-4 border-gray-300 rounded text-indigo-600 focus:ring-indigo-500"
@click="updateNewsletterStatus"
/>

<label for="newsletter-checkbox" class="pl-5 text-base mt--1">
Yes, I would like to subscribe to the free Demostore newsletter. (I
may unsubscribe at any time.)
Expand Down
31 changes: 31 additions & 0 deletions templates/vue-demo-store/pages/newsletter-subscribe.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
import { newsletterConfirmation } from "@shopware-pwa/api-client";
const route = useRoute();
const { apiInstance } = useShopwareContext();
const error = ref(false);
try {
await newsletterConfirmation(
{
em: route.query.em as string,
hash: route.query.hash as string,
},
apiInstance
);
} catch (e) {
error.value = true;
}
</script>
<template>
<div class="max-w-screen-xl mx-auto px-6 sm:px-4">
<h1 class="text-3xl mb-3">Newsletter subscription</h1>
<div class="text-xl" :class="{ 'text-red': error, 'text-green': !error }">
{{
error
? "Something goes wrong please try again later"
: "Thank you! We have signed up your address."
}}
</div>
</div>
</template>

2 comments on commit e13d3d9

@vercel
Copy link

@vercel vercel bot commented on e13d3d9 Mar 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

frontends-demo – ./templates/vue-demo-store

frontends-demo-git-main-shopware-frontends.vercel.app
frontends-demo-shopware-frontends.vercel.app
frontends-demo.vercel.app

@vercel
Copy link

@vercel vercel bot commented on e13d3d9 Mar 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.