feat(shopify): Support Shopify-initiated install flow and improve integration page

This commit is contained in:
Muhsin
2026-02-13 13:14:04 +05:30
parent 2c2f0547f7
commit 434d621dc4
14 changed files with 163 additions and 31 deletions
@@ -12,6 +12,12 @@ class ShopifyAPI extends ApiClient {
params: { contact_id: contactId },
});
}
completeInstall(pendingInstallToken) {
return axios.post(`${this.url}/complete_install`, {
pending_install_token: pendingInstallToken,
});
}
}
export default new ShopifyAPI();
@@ -21,6 +21,7 @@ const FEATURE_HELP_URLS = {
billing: 'https://chwt.app/pricing',
saml: 'https://chwt.app/hc/saml',
captain_billing: 'https://chwt.app/hc/captain_billing',
shopify: 'https://chwt.app/hc/shopify',
};
export function getHelpUrlForFeature(featureName) {
@@ -10,9 +10,18 @@
"LABEL": "Store URL",
"PLACEHOLDER": "your-store.myshopify.com",
"HELP": "Enter your Shopify store's myshopify.com URL",
"INVALID_URL": "Please enter a valid Shopify store URL (e.g., your-store.myshopify.com)",
"CANCEL": "Cancel",
"SUBMIT": "Connect Store"
},
"PENDING_INSTALL": {
"SUCCESS": "Shopify integration connected successfully.",
"ERROR": "Failed to complete Shopify installation. The link may have expired."
},
"HELP_TEXT": {
"TITLE": "How to use the Shopify Integration?",
"BODY": "With this integration, your Shopify store ***{storeDomain}*** is connected to your Chatwoot workspace. Here's what you can do:\n\n**Track orders in conversations:** When you open a conversation, the Shopify sidebar will automatically display recent orders for the customer based on their email address. This gives your support team instant context without switching tabs.\n\n**Access order details:** View order status, fulfillment status, total amount, and individual line items directly within the conversation panel."
},
"ERROR": "There was an error connecting to Shopify. Please try again or contact support if the issue persists."
},
"HEADER": "Integrations",
@@ -1,13 +1,18 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import {
useFunctionGetter,
useMapGetter,
useStore,
} from 'dashboard/composables/store';
import { useAlert } from 'dashboard/composables';
import { useMessageFormatter } from 'shared/composables/useMessageFormatter';
import Integration from './Integration.vue';
import Spinner from 'shared/components/Spinner.vue';
import integrationAPI from 'dashboard/api/integrations';
import shopifyAPI from 'dashboard/api/integrations/shopify';
import Input from 'dashboard/components-next/input/Input.vue';
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
@@ -21,6 +26,10 @@ defineProps({
});
const store = useStore();
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const { formatMessage } = useMessageFormatter();
const dialogRef = ref(null);
const integrationLoaded = ref(false);
const storeUrl = ref('');
@@ -29,6 +38,14 @@ const storeUrlError = ref('');
const integration = useFunctionGetter('integrations/getIntegration', 'shopify');
const uiFlags = useMapGetter('integrations/getUIFlags');
const hook = computed(() => {
const { hooks = [] } = integration.value || {};
const [firstHook] = hooks;
return firstHook || {};
});
const storeDomain = computed(() => hook.value.reference_id || '');
const integrationAction = computed(() => {
if (integration.value.enabled) {
return 'disconnect';
@@ -36,6 +53,15 @@ const integrationAction = computed(() => {
return 'connect';
});
const formattedHelpText = computed(() => {
return formatMessage(
t('INTEGRATION_SETTINGS.SHOPIFY.HELP_TEXT.BODY', {
storeDomain: storeDomain.value,
}),
false
);
});
const hideStoreUrlModal = () => {
storeUrl.value = '';
storeUrlError.value = '';
@@ -57,8 +83,9 @@ const handleStoreUrlSubmit = async () => {
try {
storeUrlError.value = '';
if (!validateStoreUrl(storeUrl.value)) {
storeUrlError.value =
'Please enter a valid Shopify store URL (e.g., your-store.myshopify.com)';
storeUrlError.value = t(
'INTEGRATION_SETTINGS.SHOPIFY.STORE_URL.INVALID_URL'
);
return;
}
@@ -77,9 +104,26 @@ const handleStoreUrlSubmit = async () => {
}
};
const completePendingInstall = async token => {
try {
await shopifyAPI.completeInstall(token);
await store.dispatch('integrations/get', 'shopify');
useAlert(t('INTEGRATION_SETTINGS.SHOPIFY.PENDING_INSTALL.SUCCESS'));
} catch {
useAlert(t('INTEGRATION_SETTINGS.SHOPIFY.PENDING_INSTALL.ERROR'));
} finally {
router.replace({ query: {} });
}
};
const initializeShopifyIntegration = async () => {
await store.dispatch('integrations/get', 'shopify');
integrationLoaded.value = true;
const pendingInstallToken = route.query.shopify_pending_install;
if (pendingInstallToken) {
await completePendingInstall(pendingInstallToken);
}
};
onMounted(() => {
@@ -88,7 +132,7 @@ onMounted(() => {
</script>
<template>
<div class="flex-grow flex-shrink p-4 overflow-auto max-w-6xl mx-auto">
<div class="overflow-auto flex-grow flex-shrink p-4 mx-auto max-w-6xl">
<div
v-if="integrationLoaded && !uiFlags.isCreatingShopify"
class="flex flex-col gap-6"
@@ -113,9 +157,22 @@ onMounted(() => {
/>
</template>
</Integration>
<div
v-if="integration.enabled"
class="flex-1 px-6 py-5 w-full rounded-md shadow outline outline-n-container outline-1 bg-n-alpha-3"
>
<div class="max-w-5xl prose-lg">
<h5 class="tracking-tight text-n-slate-12">
{{ $t('INTEGRATION_SETTINGS.SHOPIFY.HELP_TEXT.TITLE') }}
</h5>
<div v-dompurify-html="formattedHelpText" class="text-n-slate-11" />
</div>
</div>
<div
v-if="error"
class="flex items-center justify-center flex-1 outline outline-n-container outline-1 bg-n-alpha-3 rounded-md shadow p-6"
class="flex flex-1 justify-center items-center p-6 rounded-md shadow outline outline-n-container outline-1 bg-n-alpha-3"
>
<p class="text-n-ruby-9">
{{ $t('INTEGRATION_SETTINGS.SHOPIFY.ERROR') }}
@@ -144,7 +201,7 @@ onMounted(() => {
</Dialog>
</div>
<div v-else class="flex items-center justify-center flex-1">
<div v-else class="flex flex-1 justify-center items-center">
<Spinner size="" color-scheme="primary" />
</div>
</div>
+4
View File
@@ -28,6 +28,10 @@ export const validateAuthenticateRoutePermission = (to, next) => {
}
if (to.name === 'no_accounts' || !to.name) {
const { redirect_url: redirectUrl } = to.query || {};
if (redirectUrl) {
return next(frontendURL(`accounts/${accountId}/${redirectUrl}`));
}
return next(frontendURL(`accounts/${accountId}/dashboard`));
}
+2
View File
@@ -12,6 +12,7 @@ import {
export const login = async ({
ssoAccountId,
ssoConversationId,
redirectUrl,
...credentials
}) => {
try {
@@ -31,6 +32,7 @@ export const login = async ({
window.location = getLoginRedirectURL({
ssoAccountId,
ssoConversationId,
redirectUrl,
user: response.data.data,
});
return null;
+8
View File
@@ -40,8 +40,16 @@ export const getCredentialsFromEmail = email => {
export const getLoginRedirectURL = ({
ssoAccountId,
ssoConversationId,
redirectUrl,
user,
}) => {
if (redirectUrl) {
const { accounts = [], account_id = null } = user || {};
const accountId = account_id || accounts[0]?.id;
if (accountId) {
return frontendURL(`accounts/${accountId}/${redirectUrl}`);
}
}
const accountPath = getSSOAccountPath({ ssoAccountId, user });
if (accountPath) {
if (ssoConversationId) {
+5 -1
View File
@@ -29,7 +29,11 @@ export const validateRouteAccess = (to, next, chatwootConfig = {}) => {
// Redirect to dashboard if a cookie is present, the cookie
// cleanup and token validation happens in the application pack.
if (hasAuthCookie()) {
replaceRouteWithReload(DEFAULT_REDIRECT_URL);
const { redirect_url: redirectUrl } = to.query || {};
const redirectTarget = redirectUrl
? `${DEFAULT_REDIRECT_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`
: DEFAULT_REDIRECT_URL;
replaceRouteWithReload(redirectTarget);
return;
}
+2
View File
@@ -43,6 +43,7 @@ export default {
ssoConversationId: { type: String, default: '' },
email: { type: String, default: '' },
authError: { type: String, default: '' },
redirectUrl: { type: String, default: '' },
},
setup() {
const { replaceInstallationName } = useBranding();
@@ -169,6 +170,7 @@ export default {
sso_auth_token: this.ssoAuthToken,
ssoAccountId: this.ssoAccountId,
ssoConversationId: this.ssoConversationId,
redirectUrl: this.redirectUrl,
};
login(credentials)
+1
View File
@@ -19,6 +19,7 @@ export default [
ssoAccountId: route.query.sso_account_id,
ssoConversationId: route.query.sso_conversation_id,
authError: route.query.error,
redirectUrl: route.query.redirect_url,
}),
},
{