diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json
index 3d5f13235ce7..9de5ffc8aa64 100644
--- a/.eslintrc-auto-import.json
+++ b/.eslintrc-auto-import.json
@@ -64,7 +64,6 @@
"useCssModule": true,
"useCssVars": true,
"useDelegate": true,
- "useEmailSubscription": true,
"useEns": true,
"useExtendedSpaces": true,
"useFlashNotification": true,
@@ -114,6 +113,8 @@
"watchSyncEffect": true,
"watchTxStatus": true,
"toValue": true,
- "useFlaggedMessageStatus": true
+ "useFlaggedMessageStatus": true,
+ "useEmailSubscription": true,
+ "useEmailFetchClient": true
}
}
diff --git a/package.json b/package.json
index d98c85273afb..bbeccba31b6d 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
"@snapshot-labs/lock": "^0.1.1015",
"@snapshot-labs/pineapple": "^0.1.0-beta.1",
"@snapshot-labs/snapshot.js": "^0.4.86",
- "@snapshot-labs/tune": "^0.1.21",
+ "@snapshot-labs/tune": "0.1.24",
"@vue/apollo-composable": "4.0.0-beta.4",
"@vueuse/core": "^10.1.2",
"@vueuse/head": "^1.1.26",
diff --git a/src/components/MenuAccount.vue b/src/components/MenuAccount.vue
index dc98fbcc17f6..309bb115d31b 100644
--- a/src/components/MenuAccount.vue
+++ b/src/components/MenuAccount.vue
@@ -8,8 +8,12 @@ const { t } = useI18n();
const { domain } = useApp();
const { logout } = useWeb3();
+const { isSubscribed, loadEmailSubscriptions } = useEmailSubscription();
+
+onMounted(loadEmailSubscriptions);
+
const router = useRouter();
-const modalEmailSubscriptionOpen = ref(false);
+const showModalEmail = ref(false);
function handleAction(e) {
if (e === 'viewProfile')
@@ -26,7 +30,7 @@ function handleAction(e) {
name: 'delegate'
});
if (e === 'subscribeEmail') {
- modalEmailSubscriptionOpen.value = true;
+ showModalEmail.value = true;
return true;
}
return logout();
@@ -53,7 +57,9 @@ function handleAction(e) {
extras: { icon: 'switch' }
},
{
- text: t('emailSubscription.subscribe'),
+ text: isSubscribed
+ ? t('emailSubscription.manage')
+ : t('emailSubscription.subscribe'),
action: 'subscribeEmail',
extras: { icon: 'mail' }
},
@@ -89,11 +95,17 @@ function handleAction(e) {
+
+
diff --git a/src/components/ModalEmailManagement.vue b/src/components/ModalEmailManagement.vue
new file mode 100644
index 000000000000..e170ff2cef49
--- /dev/null
+++ b/src/components/ModalEmailManagement.vue
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
{{ $t('emailManagement.title') }}
+
+
+
+
+
+ {{ t('emailManagement.subtitle') }}
+
+
+
+
+
+
diff --git a/src/components/ModalEmailSubscription.vue b/src/components/ModalEmailSubscription.vue
index 84eeee22d5d2..4295f0674640 100644
--- a/src/components/ModalEmailSubscription.vue
+++ b/src/components/ModalEmailSubscription.vue
@@ -1,22 +1,29 @@
@@ -27,7 +34,7 @@ function submit() {
{{ $t('emailSubscription.title') }}
-
+
@@ -46,7 +53,7 @@ function submit() {
{{ $t('emailSubscription.description') }}
-
-
+
{{ $t('close') }}
diff --git a/src/components/NavbarAccount.vue b/src/components/NavbarAccount.vue
index ea5b5d8f3fd4..b4e1e92d91cc 100644
--- a/src/components/NavbarAccount.vue
+++ b/src/components/NavbarAccount.vue
@@ -25,7 +25,7 @@ watchEffect(() => {
-
+
{
+ const wallet = aliasWallet.value;
+ const address = aliasWallet.value.address;
+ return sign(wallet, address, message, typesSchema);
+ });
+ }
+
+ const fetchSubscriptions = body => {
+ return useEmailFetch('/subscriber').post(body).json();
+ };
+
+ const subscribeWithEmail = async unsignedParams => {
+ const signature = await signWithAlias(unsignedParams, SubscribeSchema);
+ const body = {
+ method: 'snapshot.subscribe',
+ params: {
+ ...unsignedParams,
+ signature
+ }
+ };
+
+ return useEmailFetch('/').post(body).json();
+ };
+
+ const updateEmailSubscriptions = async unsignedParams => {
+ const signature = await signWithAlias(
+ unsignedParams,
+ UpdateSubscriptionsSchema
+ );
+ const body = {
+ method: 'snapshot.update',
+ params: {
+ ...unsignedParams,
+ signature
+ }
+ };
+
+ return useEmailFetch('/').post(body).json();
+ };
+
+ return {
+ fetchSubscriptions,
+ subscribeWithEmail,
+ updateEmailSubscriptions
+ };
+}
diff --git a/src/composables/useEmailSubscription.ts b/src/composables/useEmailSubscription.ts
index 8a0b61ae355c..446b4551225e 100644
--- a/src/composables/useEmailSubscription.ts
+++ b/src/composables/useEmailSubscription.ts
@@ -1,70 +1,88 @@
-import sign, { SubscribeType } from '@/helpers/sign';
-import { getInstance } from '@snapshot-labs/lock/plugins/vue3';
+import { createSharedComposable } from '@vueuse/core';
-export function useEmailSubscription() {
- enum Status {
- waiting,
- success,
- error
- }
+enum Status {
+ waiting,
+ success,
+ error
+}
- enum Level {
- info = 'info',
- warning = 'warning',
- 'warning-red' = 'warning-red',
- success = 'success'
- }
+enum Level {
+ info = 'info',
+ warning = 'warning',
+ 'warning-red' = 'warning-red',
+ success = 'success'
+}
- const status: Ref = ref(Status.waiting);
- const postSubscribeLevel: Ref = ref(Level.info);
- const postSubscribeMessage = ref('');
- const loading = ref(false);
+const subscriptionTypes = ['summary', 'newProposal', 'closedProposal'] as const;
+type SubscriptionType = (typeof subscriptionTypes)[number];
+function useEmailSubscriptionComposable() {
const { t } = useI18n();
- const { web3 } = useWeb3();
- const auth = getInstance();
- async function subscribe(email: string, address: string) {
- loading.value = true;
+ const status = ref(Status.waiting);
+ const isSuccessful = computed(() => status.value === Status.success);
+ const postSubscribeLevel = ref(Level.info);
+ const postSubscribeMessage = ref('');
+ const loading = ref(false);
+ const isSubscribed = ref(false);
+ const shouldRemoveEmail = ref(true);
+ const { aliasWallet } = useAliasAction();
+ const { fetchSubscriptions, subscribeWithEmail, updateEmailSubscriptions } =
+ useEmailFetchClient();
- try {
- const signature = await sign(
- auth.web3,
- web3.value.account,
- {
- email
- },
- SubscribeType
- );
+ const apiSubscriptions = ref([]);
+ const clientSubscriptions = computed({
+ get() {
+ return subscriptionTypes.reduce((acc, type) => {
+ acc[type] = apiSubscriptions.value.includes(type);
+ return acc;
+ }, {} as Record);
+ },
+ set(value) {
+ apiSubscriptions.value = Object.entries(value)
+ .map(([key, value]) => (value ? key : undefined))
+ .filter(Boolean)
+ .map(key => key as SubscriptionType);
+ }
+ });
- const response = await fetch(import.meta.env.VITE_ENVELOP_URL, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- params: {
- email,
- address,
- signature
- },
- method: 'snapshot.subscribe'
- })
- });
+ const loadEmailSubscriptions = async () => {
+ loading.value = true;
+ const { error, data } = await fetchSubscriptions({
+ address: aliasWallet.value.address
+ });
+ loading.value = false;
+ if (!error.value && data.value) {
+ apiSubscriptions.value = data.value;
+ isSubscribed.value = true;
+ }
+ };
- const result = await response.json();
+ const subscribe = async (email: string) => {
+ loading.value = true;
+ const { data } = await subscribeWithEmail({
+ address: aliasWallet.value.address,
+ email
+ });
+ loading.value = false;
- if (result.result === 'OK') {
- setPostSubscribeState(Status.success, Level.success, 'success');
- } else {
- setPostSubscribeState(Status.error, Level.warning, 'apiError');
- }
- } catch (e) {
- setPostSubscribeState(Status.error, Level.warning, 'unknownError');
- } finally {
- loading.value = false;
+ if (data.value?.result === 'OK') {
+ setPostSubscribeState(Status.success, Level.success, 'success');
+ } else {
+ setPostSubscribeState(Status.error, Level.warning, 'error');
}
- }
+ };
+
+ const updateSubscriptions = async () => {
+ loading.value = true;
+ await updateEmailSubscriptions({
+ address: aliasWallet.value.address,
+ email: '',
+ subscriptions: apiSubscriptions.value
+ });
+ loading.value = false;
+ loadEmailSubscriptions();
+ };
function setPostSubscribeState(
newStatus: Status,
@@ -83,14 +101,23 @@ export function useEmailSubscription() {
}
return {
+ clientSubscriptions,
+ shouldRemoveEmail,
subscribe,
+ updateSubscriptions,
+ loadEmailSubscriptions,
status,
loading,
reset,
+ isSuccessful,
+ isSubscribed,
postSubscribeState: {
level: postSubscribeLevel,
message: postSubscribeMessage
- },
- Status
+ }
};
}
+
+export const useEmailSubscription = createSharedComposable(
+ useEmailSubscriptionComposable
+);
diff --git a/src/helpers/sign.ts b/src/helpers/sign.ts
index b6e7d63b9da3..7e1b938237bb 100644
--- a/src/helpers/sign.ts
+++ b/src/helpers/sign.ts
@@ -7,20 +7,11 @@ const domain = {
version: '0.1.4'
};
-type DataType = Record;
+export type DataType = Record;
-export const SubscribeType: DataType = {
- Subscribe: [
- { name: 'from', type: 'address' },
- { name: 'email', type: 'string' }
- ]
-};
-
-export interface ISubscribe {
- from?: string;
- timestamp?: number;
- email: string;
-}
+export type ISubscribe = {
+ address: string;
+} & Record;
export default async function sign(
web3: Web3Provider | Wallet,
@@ -29,8 +20,6 @@ export default async function sign(
types: DataType
) {
const signer = 'getSigner' in web3 ? web3.getSigner() : web3;
- const checksumAddress = getAddress(address);
- message.from = message.from ? getAddress(message.from) : checksumAddress;
-
+ message.address = getAddress(address);
return await signer._signTypedData(domain, types, message);
}
diff --git a/src/locales/default.json b/src/locales/default.json
index acf3d84c6e7c..666b1f6c3def 100644
--- a/src/locales/default.json
+++ b/src/locales/default.json
@@ -914,6 +914,7 @@
"emailSubscription": {
"title": "Email subscription",
"subscribe": "Subscribe",
+ "manage": "Manage subscriptions",
"description": "Subscribe to receive email notifications about your joined spaces and proposals activities.",
"inputCaption": "You may be asked to sign a message to verify wallet ownership",
"postSubscribeMessage": {
@@ -922,6 +923,18 @@
"unknownError": "Unable to subscribe to email notifications, please try again later"
}
},
+ "emailManagement": {
+ "title": "Subscription management",
+ "subtitle": "Choose the types of email updates that matter to you:",
+ "optionNewProposal": "Proposal creation",
+ "optionNewProposalDescription": "Get informed when a new proposal is submitted in your followed spaces.",
+ "optionClosedProposal": "Proposal closure",
+ "optionClosedProposalDescription": "Get informed when a proposal is closed in your followed spaces.",
+ "optionSummary": "Weekly summary",
+ "optionSummaryDescription": "Get a weekly report detailing the activity in your followed spaces.",
+ "removeEmail": "Also remove my email from Snapshot's database",
+ "updatePreferences": "Update preferences"
+ },
"joinCommunity": "Join Snapshot community",
"header": {
"title": "Where decisions get made",
diff --git a/src/style.scss b/src/style.scss
index 9e64a6c305f6..dff9968d4c4f 100644
--- a/src/style.scss
+++ b/src/style.scss
@@ -117,6 +117,10 @@
@apply font-sans;
}
+.tune-input-checkbox {
+ @apply rounded-lg;
+}
+
.tune-button {
@apply rounded-full;
}
diff --git a/yarn.lock b/yarn.lock
index a18516f44f12..801e9be1a651 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1106,7 +1106,16 @@
dependencies:
"@hapi/hoek" "^9.0.0"
-"@headlessui-float/vue@^0.11.0", "@headlessui-float/vue@^0.11.1":
+"@headlessui-float/vue@^0.11.0":
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/@headlessui-float/vue/-/vue-0.11.2.tgz#6ecf6bedd6bbce3aebcc74f5c2c2dcf7c108289b"
+ integrity sha512-9yWN6LHeaXZKyU9y/rj6SmujCzkRxxf4bhg7nRs2RSwfSRI9E+JPvM/Tl7AA2lFQAJegWTRcElt3dSJ0Nkth0Q==
+ dependencies:
+ "@floating-ui/core" "^1.0.0"
+ "@floating-ui/dom" "^1.0.0"
+ "@floating-ui/vue" "^0.2.0"
+
+"@headlessui-float/vue@^0.11.1":
version "0.11.1"
resolved "https://registry.yarnpkg.com/@headlessui-float/vue/-/vue-0.11.1.tgz#d089e5a9d3b244c8046c0e176e3ad537f29b5c7c"
integrity sha512-QWqyfNLhWimCVfmCpv/NBNyhpads1ubkOmuCob8RHD1DO+TQEY0DDg3cy8zBC75EOqWfTEdA94jPGlM5UnIthw==
@@ -1562,10 +1571,10 @@
json-to-graphql-query "^2.2.4"
lodash.set "^4.3.2"
-"@snapshot-labs/tune@^0.1.21":
- version "0.1.21"
- resolved "https://registry.yarnpkg.com/@snapshot-labs/tune/-/tune-0.1.21.tgz#9715b895a2bdc56156e8ea67d7f63fbe8f37452e"
- integrity sha512-M2W3QFdSH7cAim20+jPCBITiylu8SSR6afHGL/diy8FPu4MxUSMfV9zVwxhGhilNCKtp9wpCS8fX8EGORmf8Kw==
+"@snapshot-labs/tune@0.1.24":
+ version "0.1.24"
+ resolved "https://registry.yarnpkg.com/@snapshot-labs/tune/-/tune-0.1.24.tgz#4a1e7f80c8e5d3a17a6ac309a73b0fd291b46d9c"
+ integrity sha512-RJN6fmPgcXfQE5ofPT1ysjVQtDk2Ml6qvhJu+jEwWdB3cJ9blfqrnRmSyQSq8wGjh6tBAmYI3J3EfuIqSgDt5Q==
dependencies:
"@headlessui-float/vue" "^0.11.0"
"@headlessui/vue" "^1.7.12"
@@ -10047,9 +10056,9 @@ minipass@^2.6.0, minipass@^2.9.0:
yallist "^3.0.0"
minisearch@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-6.0.1.tgz#55e40135e7e6be60f1c1c2f5ee890c334e179a86"
- integrity sha512-Ly1w0nHKnlhAAh6/BF/+9NgzXfoJxaJ8nhopFhQ3NcvFJrFIL+iCg9gw9e9UMBD+XIsp/RyznJ/o5UIe5Kw+kg==
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-6.1.0.tgz#6e74e743dbd0e88fa5ca52fef2391e0aa7055252"
+ integrity sha512-PNxA/X8pWk+TiqPbsoIYH0GQ5Di7m6326/lwU/S4mlo4wGQddIcf/V//1f9TB0V4j59b57b+HZxt8h3iMROGvg==
minizlib@^1.3.3:
version "1.3.3"