Merge PR #14501 google play reviews channel
# Conflicts: # app/controllers/api/v1/accounts/inboxes_controller.rb # app/helpers/api/v1/inboxes_helper.rb # app/javascript/dashboard/components-next/icon/provider.js # app/javascript/dashboard/components-next/message/MessageMeta.vue # app/javascript/dashboard/composables/useInbox.js # app/javascript/dashboard/constants/editor.js # app/javascript/dashboard/helper/inbox.js # app/javascript/dashboard/i18n/locale/en/inboxMgmt.json # app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelFactory.vue # app/javascript/dashboard/routes/dashboard/settings/inbox/ChannelList.vue # app/javascript/dashboard/routes/dashboard/settings/inbox/Settings.vue # app/javascript/dashboard/routes/dashboard/settings/inbox/components/ChannelName.vue # app/javascript/shared/mixins/inboxMixin.js # app/views/api/v1/models/_inbox.json.jbuilder # config/schedule.yml # db/schema.rb
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class GooglePlayClient extends ApiClient {
|
||||
constructor() {
|
||||
super('google_play', { accountScoped: true });
|
||||
}
|
||||
|
||||
generateAuthorization(payload) {
|
||||
return axios.post(`${this.url}/authorization`, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default new GooglePlayClient();
|
||||
@@ -16,6 +16,7 @@ export function useChannelIcon(inbox) {
|
||||
'Channel::Instagram': 'i-woot-instagram',
|
||||
'Channel::Tiktok': 'i-woot-tiktok',
|
||||
'Channel::AppStore': 'i-ri-app-store-fill',
|
||||
'Channel::GooglePlay': 'i-ri-google-play-line',
|
||||
};
|
||||
|
||||
const providerIconMap = {
|
||||
|
||||
@@ -22,6 +22,7 @@ const {
|
||||
isAnInstagramChannel,
|
||||
isAnAppStoreChannel,
|
||||
isATiktokChannel,
|
||||
isAGooglePlayChannel,
|
||||
} = useInbox();
|
||||
|
||||
const {
|
||||
@@ -64,7 +65,8 @@ const isSent = computed(() => {
|
||||
isATelegramChannel.value ||
|
||||
isAnInstagramChannel.value ||
|
||||
isATiktokChannel.value ||
|
||||
isAnAppStoreChannel.value
|
||||
isAnAppStoreChannel.value ||
|
||||
isAGooglePlayChannel.value
|
||||
) {
|
||||
return sourceId.value && status.value === MESSAGE_STATUS.SENT;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ const isActive = computed(() => {
|
||||
'instagram',
|
||||
'tiktok',
|
||||
'voice',
|
||||
'google_play',
|
||||
].includes(key);
|
||||
});
|
||||
|
||||
|
||||
@@ -264,6 +264,9 @@ export default {
|
||||
if (this.isATiktokChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TIKTOK;
|
||||
}
|
||||
if (this.isAGooglePlayChannel) {
|
||||
return MESSAGE_MAX_LENGTH.GOOGLE_PLAY;
|
||||
}
|
||||
if (this.isATwilioWhatsAppChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ const mockStore = createStore({
|
||||
voice_enabled: true,
|
||||
},
|
||||
15: { id: 15, channel_type: INBOX_TYPES.TIKTOK },
|
||||
16: { id: 16, channel_type: INBOX_TYPES.GOOGLE_PLAY },
|
||||
};
|
||||
return inboxes[id] || null;
|
||||
},
|
||||
@@ -226,6 +227,12 @@ describe('useInbox', () => {
|
||||
global: { plugins: [mockStore] },
|
||||
});
|
||||
expect(wrapper.vm.isATiktokChannel).toBe(true);
|
||||
|
||||
// Test Google Play
|
||||
wrapper = mount(createTestComponent(16), {
|
||||
global: { plugins: [mockStore] },
|
||||
});
|
||||
expect(wrapper.vm.isAGooglePlayChannel).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -278,6 +285,7 @@ describe('useInbox', () => {
|
||||
'isAnEmailChannel',
|
||||
'isAnInstagramChannel',
|
||||
'isATiktokChannel',
|
||||
'isAGooglePlayChannel',
|
||||
'voiceCallEnabled',
|
||||
'voiceCallProvider',
|
||||
];
|
||||
|
||||
@@ -144,6 +144,10 @@ export const useInbox = (inboxId = null) => {
|
||||
return channelType.value === INBOX_TYPES.APP_STORE;
|
||||
});
|
||||
|
||||
const isAGooglePlayChannel = computed(() => {
|
||||
return channelType.value === INBOX_TYPES.GOOGLE_PLAY;
|
||||
});
|
||||
|
||||
const voiceCallEnabled = computed(() => isVoiceCallEnabled(inbox.value));
|
||||
|
||||
const voiceCallProvider = computed(() => getVoiceCallProvider(inbox.value));
|
||||
@@ -167,6 +171,7 @@ export const useInbox = (inboxId = null) => {
|
||||
isAnInstagramChannel,
|
||||
isATiktokChannel,
|
||||
isAnAppStoreChannel,
|
||||
isAGooglePlayChannel,
|
||||
voiceCallEnabled,
|
||||
voiceCallProvider,
|
||||
};
|
||||
|
||||
@@ -119,6 +119,11 @@ export const FORMATTING = {
|
||||
nodes: [],
|
||||
menu: [],
|
||||
},
|
||||
'Channel::GooglePlay': {
|
||||
marks: [],
|
||||
nodes: [],
|
||||
menu: [],
|
||||
},
|
||||
// Special contexts (not actual channels)
|
||||
'Context::PrivateNote': {
|
||||
marks: ['strong', 'em', 'code', 'link', 'strike'],
|
||||
|
||||
@@ -12,6 +12,7 @@ export const INBOX_TYPES = {
|
||||
INSTAGRAM: 'Channel::Instagram',
|
||||
TIKTOK: 'Channel::Tiktok',
|
||||
APP_STORE: 'Channel::AppStore',
|
||||
GOOGLE_PLAY: 'Channel::GooglePlay',
|
||||
};
|
||||
|
||||
// Add providers here as they gain voice capability (e.g., WhatsApp Cloud, Twilio WhatsApp)
|
||||
@@ -52,6 +53,7 @@ const INBOX_ICON_MAP_FILL = {
|
||||
[INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-fill',
|
||||
[INBOX_TYPES.TIKTOK]: 'i-ri-tiktok-fill',
|
||||
[INBOX_TYPES.APP_STORE]: 'i-ri-app-store-fill',
|
||||
[INBOX_TYPES.GOOGLE_PLAY]: 'i-ri-google-play-fill',
|
||||
};
|
||||
|
||||
const DEFAULT_ICON_FILL = 'i-ri-chat-1-fill';
|
||||
@@ -68,6 +70,7 @@ const INBOX_ICON_MAP_LINE = {
|
||||
[INBOX_TYPES.INSTAGRAM]: 'i-woot-instagram',
|
||||
[INBOX_TYPES.TIKTOK]: 'i-woot-tiktok',
|
||||
[INBOX_TYPES.APP_STORE]: 'i-ri-app-store-line',
|
||||
[INBOX_TYPES.GOOGLE_PLAY]: 'i-ri-google-play-line',
|
||||
};
|
||||
|
||||
const DEFAULT_ICON_LINE = 'i-ri-chat-1-line';
|
||||
@@ -120,6 +123,9 @@ export const getReadableInboxByType = (type, phoneNumber) => {
|
||||
case INBOX_TYPES.APP_STORE:
|
||||
return 'app_store';
|
||||
|
||||
case INBOX_TYPES.GOOGLE_PLAY:
|
||||
return 'google_play';
|
||||
|
||||
default:
|
||||
return 'chat';
|
||||
}
|
||||
|
||||
@@ -480,6 +480,24 @@
|
||||
"ERROR_MESSAGE": "We were not able to save the telegram channel"
|
||||
}
|
||||
},
|
||||
"GOOGLE_PLAY": {
|
||||
"TITLE": "Google Play Reviews",
|
||||
"DESC": "Manage and reply to your Google Play Store app reviews from Chatwoot.",
|
||||
"INBOX_NAME": {
|
||||
"LABEL": "Inbox Name",
|
||||
"PLACEHOLDER": "Please enter an inbox name",
|
||||
"ERROR": "This field is required"
|
||||
},
|
||||
"APP_ID": {
|
||||
"LABEL": "App package name",
|
||||
"PLACEHOLDER": "Please enter your app package name (eg: com.example.app)",
|
||||
"ERROR": "Please enter a valid app package name"
|
||||
},
|
||||
"CONNECT_BUTTON": "Connect with Google",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to connect your Google Play Reviews channel"
|
||||
}
|
||||
},
|
||||
"AUTH": {
|
||||
"TITLE": "Choose a channel",
|
||||
"DESC": "Chatwoot supports live-chat widgets, Facebook Messenger, WhatsApp, Emails, etc., as channels. If you want to build a custom channel, you can create it using the API channel. To get started, choose one of the channels below.",
|
||||
@@ -533,6 +551,10 @@
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
},
|
||||
"GOOGLE_PLAY": {
|
||||
"TITLE": "Google Play Reviews",
|
||||
"DESCRIPTION": "Manage your Google Play Store app reviews"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1203,6 +1225,7 @@
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"APP_STORE": "App Store Reviews",
|
||||
"GOOGLE_PLAY": "Google Play Reviews",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import Instagram from './channels/Instagram.vue';
|
||||
import Tiktok from './channels/Tiktok.vue';
|
||||
import Voice from './channels/Voice.vue';
|
||||
import AppStore from './channels/AppStore.vue';
|
||||
import GooglePlay from './channels/GooglePlay.vue';
|
||||
|
||||
const channelViewList = {
|
||||
facebook: Facebook,
|
||||
@@ -28,6 +29,7 @@ const channelViewList = {
|
||||
tiktok: Tiktok,
|
||||
app_store: AppStore,
|
||||
voice: Voice,
|
||||
google_play: GooglePlay,
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
|
||||
@@ -83,6 +83,12 @@ const channelList = computed(() => {
|
||||
description: t('INBOX_MGMT.ADD.AUTH.CHANNEL.APP_STORE.DESCRIPTION'),
|
||||
icon: 'i-ri-app-store-line',
|
||||
},
|
||||
{
|
||||
key: 'google_play',
|
||||
title: t('INBOX_MGMT.ADD.AUTH.CHANNEL.GOOGLE_PLAY.TITLE'),
|
||||
description: t('INBOX_MGMT.ADD.AUTH.CHANNEL.GOOGLE_PLAY.DESCRIPTION'),
|
||||
icon: 'i-ri-google-play-line',
|
||||
},
|
||||
];
|
||||
|
||||
if (hasTiktokConfigured.value) {
|
||||
|
||||
@@ -249,7 +249,7 @@ export default {
|
||||
];
|
||||
}
|
||||
|
||||
if (this.isAnAppStoreChannel) {
|
||||
if (this.isAnAppStoreChannel || this.isAGooglePlayChannel) {
|
||||
const unsupportedKeys = ['business-hours', 'csat', 'bot-configuration'];
|
||||
visibleToAllChannelTabs = visibleToAllChannelTabs.filter(
|
||||
tab => !unsupportedKeys.includes(tab.key)
|
||||
@@ -289,7 +289,10 @@ export default {
|
||||
if (this.isAnEmailChannel) {
|
||||
return `${this.inbox.name} (${this.inbox.email})`;
|
||||
}
|
||||
if (this.isAnAppStoreChannel && this.inbox.app_id) {
|
||||
if (
|
||||
(this.isAnAppStoreChannel || this.isAGooglePlayChannel) &&
|
||||
this.inbox.app_id
|
||||
) {
|
||||
return `${this.inbox.name} (${this.inbox.app_id})`;
|
||||
}
|
||||
return this.inbox.name;
|
||||
@@ -840,7 +843,7 @@ export default {
|
||||
</SettingsFieldSection>
|
||||
|
||||
<SettingsFieldSection
|
||||
v-if="!isAnAppStoreChannel"
|
||||
v-if="!isAnAppStoreChannel && !isAGooglePlayChannel"
|
||||
:label="$t('INBOX_MGMT.HELP_CENTER.LABEL')"
|
||||
:help-text="$t('INBOX_MGMT.HELP_CENTER.SUB_TEXT')"
|
||||
>
|
||||
@@ -1140,7 +1143,7 @@ export default {
|
||||
</SettingsAccordion>
|
||||
|
||||
<SettingsAccordion
|
||||
v-if="!isAnAppStoreChannel"
|
||||
v-if="!isAnAppStoreChannel && !isAGooglePlayChannel"
|
||||
:title="$t('INBOX_MGMT.CHANNEL_PREFERENCES')"
|
||||
class="mt-6"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVuelidate } from '@vuelidate/core';
|
||||
import { required } from '@vuelidate/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import googlePlayClient from 'dashboard/api/channel/googlePlayClient';
|
||||
import PageHeader from '../../SettingsSubPageHeader.vue';
|
||||
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
// Surface any error the OAuth callback redirected back with
|
||||
onMounted(() => {
|
||||
if (route.query.error) {
|
||||
useAlert(String(route.query.error));
|
||||
}
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
inboxName: '',
|
||||
appId: '',
|
||||
});
|
||||
|
||||
const isConnecting = ref(false);
|
||||
|
||||
const rules = {
|
||||
inboxName: { required },
|
||||
appId: { required },
|
||||
};
|
||||
|
||||
const v$ = useVuelidate(rules, state);
|
||||
|
||||
const connectWithGoogle = async () => {
|
||||
v$.value.$touch();
|
||||
if (v$.value.$invalid) return;
|
||||
|
||||
try {
|
||||
isConnecting.value = true;
|
||||
const {
|
||||
data: { url },
|
||||
} = await googlePlayClient.generateAuthorization({
|
||||
app_id: state.appId.trim(),
|
||||
inbox_name: state.inboxName.trim(),
|
||||
});
|
||||
window.location.href = url;
|
||||
} catch (error) {
|
||||
isConnecting.value = false;
|
||||
useAlert(t('INBOX_MGMT.ADD.GOOGLE_PLAY.API.ERROR_MESSAGE'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full w-full p-6 col-span-6">
|
||||
<PageHeader
|
||||
:header-title="$t('INBOX_MGMT.ADD.GOOGLE_PLAY.TITLE')"
|
||||
:header-content="$t('INBOX_MGMT.ADD.GOOGLE_PLAY.DESC')"
|
||||
/>
|
||||
<form
|
||||
class="flex flex-wrap flex-col mx-0"
|
||||
@submit.prevent="connectWithGoogle"
|
||||
>
|
||||
<div class="flex-shrink-0 flex-grow-0">
|
||||
<label :class="{ error: v$.inboxName.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.GOOGLE_PLAY.INBOX_NAME.LABEL') }}
|
||||
<input
|
||||
v-model="state.inboxName"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.GOOGLE_PLAY.INBOX_NAME.PLACEHOLDER')
|
||||
"
|
||||
@blur="v$.inboxName.$touch"
|
||||
/>
|
||||
<span v-if="v$.inboxName.$error" class="message">
|
||||
{{ $t('INBOX_MGMT.ADD.GOOGLE_PLAY.INBOX_NAME.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0 flex-grow-0">
|
||||
<label :class="{ error: v$.appId.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.GOOGLE_PLAY.APP_ID.LABEL') }}
|
||||
<input
|
||||
v-model="state.appId"
|
||||
type="text"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.GOOGLE_PLAY.APP_ID.PLACEHOLDER')"
|
||||
@blur="v$.appId.$touch"
|
||||
/>
|
||||
<span v-if="v$.appId.$error" class="message">
|
||||
{{ $t('INBOX_MGMT.ADD.GOOGLE_PLAY.APP_ID.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-4">
|
||||
<NextButton
|
||||
:is-loading="isConnecting"
|
||||
type="submit"
|
||||
solid
|
||||
blue
|
||||
:label="$t('INBOX_MGMT.ADD.GOOGLE_PLAY.CONNECT_BUTTON')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
@@ -35,6 +35,7 @@ const i18nMap = {
|
||||
'Channel::Instagram': 'INSTAGRAM',
|
||||
'Channel::Tiktok': 'TIKTOK',
|
||||
'Channel::AppStore': 'APP_STORE',
|
||||
'Channel::GooglePlay': 'GOOGLE_PLAY',
|
||||
};
|
||||
|
||||
const twilioChannelName = () => {
|
||||
|
||||
@@ -22,4 +22,6 @@ export const MESSAGE_MAX_LENGTH = {
|
||||
TELEGRAM: 4096,
|
||||
LINE: 2000,
|
||||
EMAIL: 25000,
|
||||
// https://developers.google.com/android-publisher/reviews — developer replies are capped at 350 characters
|
||||
GOOGLE_PLAY: 350,
|
||||
};
|
||||
|
||||
@@ -141,6 +141,9 @@ export default {
|
||||
isAnAppStoreChannel() {
|
||||
return this.channelType === INBOX_TYPES.APP_STORE;
|
||||
},
|
||||
isAGooglePlayChannel() {
|
||||
return this.channelType === INBOX_TYPES.GOOGLE_PLAY;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
inboxHasFeature(feature) {
|
||||
|
||||
Reference in New Issue
Block a user