-
Notifications
You must be signed in to change notification settings - Fork 5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mailchimp #1332
Mailchimp #1332
Changes from 8 commits
e4f4ef8
51ec5fd
4a7a770
84949f9
34c6650
5d55071
0305ed4
6b6aa6b
4d9a4a9
a4083db
4491f0f
3a84785
78de270
b7b41ad
6d0f6ce
f71eac0
5dbf3e4
e93b0ca
1bd3858
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { useQuery } from "@tanstack/react-query" | ||
import React, { useEffect, useState } from "react" | ||
|
||
import { fetchUserMarketingConsent, updateMarketingConsent } from "@/services/backend" | ||
import CheckBox from "@/shared-module/common/components/InputFields/CheckBox" | ||
import useToastMutation from "@/shared-module/common/hooks/useToastMutation" | ||
import { assertNotNullOrUndefined } from "@/shared-module/common/utils/nullability" | ||
|
||
interface SelectMarketingConsentFormProps { | ||
courseId: string | ||
courseLanguageGroupsId: string | ||
} | ||
|
||
const SelectMarketingConsentForm: React.FC<SelectMarketingConsentFormProps> = ({ | ||
courseId, | ||
courseLanguageGroupsId, | ||
}) => { | ||
const [marketingConsent, setMarketingConsent] = useState(false) | ||
|
||
const fetchInitialMarketingConsent = useQuery({ | ||
queryKey: ["marketing-consent", courseId], | ||
queryFn: () => fetchUserMarketingConsent(assertNotNullOrUndefined(courseId)), | ||
enabled: courseId !== undefined, | ||
}) | ||
|
||
useEffect(() => { | ||
if (fetchInitialMarketingConsent.isSuccess) { | ||
setMarketingConsent(fetchInitialMarketingConsent.data.consent) | ||
} | ||
}, [fetchInitialMarketingConsent.data, fetchInitialMarketingConsent.isSuccess]) | ||
|
||
const handleMarketingConsentChangeMutation = useToastMutation( | ||
async () => { | ||
try { | ||
await updateMarketingConsent(courseId, courseLanguageGroupsId, marketingConsent) | ||
} catch (error) { | ||
setMarketingConsent(!marketingConsent) | ||
} | ||
return null | ||
}, | ||
{ notify: false }, | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve mutation error handling and user feedback
const handleMarketingConsentChangeMutation = useToastMutation(
async () => {
+ const previousConsent = marketingConsent
try {
await updateMarketingConsent(courseId, courseLanguageGroupsId, marketingConsent)
} catch (error) {
- setMarketingConsent(!marketingConsent)
+ setMarketingConsent(previousConsent)
+ throw new Error('Failed to update marketing consent')
}
return null
},
- { notify: false },
+ {
+ notify: true,
+ errorMessage: 'Failed to update marketing consent. Please try again.',
+ },
)
|
||
|
||
return ( | ||
<> | ||
<CheckBox | ||
// eslint-disable-next-line i18next/no-literal-string | ||
label="I consent to receive marketing messages for this course." | ||
type="checkbox" | ||
checked={marketingConsent} | ||
onChange={() => { | ||
setMarketingConsent(!marketingConsent) | ||
handleMarketingConsentChangeMutation.mutate() | ||
}} | ||
></CheckBox> | ||
</> | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve accessibility and internationalization
return (
<>
<CheckBox
- // eslint-disable-next-line i18next/no-literal-string
- label="I consent to receive marketing messages for this course."
+ label={t('marketing.consent.label')}
type="checkbox"
checked={marketingConsent}
+ disabled={isLoading || handleMarketingConsentChangeMutation.isLoading}
onChange={() => {
setMarketingConsent(!marketingConsent)
handleMarketingConsentChangeMutation.mutate()
}}
></CheckBox>
+ {(isLoading || handleMarketingConsentChangeMutation.isLoading) && (
+ <LoadingSpinner size="small" />
+ )}
</>
)
|
||
} | ||
|
||
export default SelectMarketingConsentForm |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,6 +49,7 @@ import { | |
UserCourseInstanceChapterProgress, | ||
UserCourseInstanceProgress, | ||
UserCourseSettings, | ||
UserMarketingConsent, | ||
UserModuleCompletionStatus, | ||
} from "@/shared-module/common/bindings" | ||
import { | ||
|
@@ -86,6 +87,7 @@ import { | |
isUserCourseInstanceChapterProgress, | ||
isUserCourseInstanceProgress, | ||
isUserCourseSettings, | ||
isUserMarketingConsent, | ||
isUserModuleCompletionStatus, | ||
} from "@/shared-module/common/bindings.guard" | ||
import { | ||
|
@@ -743,3 +745,28 @@ export const claimCodeFromCodeGiveaway = async (id: string): Promise<string> => | |
const response = await courseMaterialClient.post(`/code-giveaways/${id}/claim`) | ||
return validateResponse(response, isString) | ||
} | ||
|
||
export const updateMarketingConsent = async ( | ||
courseId: string, | ||
courseLanguageGroupsId: string, | ||
consent: boolean, | ||
): Promise<string> => { | ||
const res = await courseMaterialClient.post( | ||
`/courses/${courseId}/user-marketing-consent`, | ||
{ | ||
course_language_groups_id: courseLanguageGroupsId, | ||
consent, | ||
}, | ||
{ | ||
responseType: "json", | ||
}, | ||
) | ||
return validateResponse(res, isString) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix the API endpoint path and consider improving the return type.
Apply this diff to fix the path: - `/courses/${courseId}/user-marketing-consent`,
+ `/courses/${courseId}/user-marketing-consent`, Consider creating a specific type for the response: type MarketingConsentUpdateResponse = {
status: string;
// Add other relevant fields based on the actual API response
} |
||
|
||
export const fetchUserMarketingConsent = async ( | ||
courseId: string, | ||
): Promise<UserMarketingConsent> => { | ||
const res = await courseMaterialClient.get(`/courses/${courseId}/fetch-user-marketing-consent`) | ||
return validateResponse(res, isUserMarketingConsent) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,6 +68,10 @@ fn main() -> Result<()> { | |
name: "chatbot-syncer", | ||
execute: Box::new(|| tokio_run(programs::chatbot_syncer::main())), | ||
}, | ||
Program { | ||
name: "mailchimp-syncer", | ||
execute: Box::new(|| tokio_run(programs::mailchimp_syncer::main())), | ||
}, | ||
Comment on lines
+71
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Codebase verification Documentation is missing for the mailchimp-syncer program The search results confirm that while there are extensive code implementations related to marketing consent and the mailchimp-syncer program, there is no operator documentation describing:
The program appears to be a critical component that synchronizes marketing consents with Mailchimp, and proper documentation would help operators maintain and troubleshoot it effectively. 🔗 Analysis chainConsider adding operator documentation for the new program. Since this is a new program that handles marketing consent synchronization with Mailchimp, it would be helpful to document:
Let's check if documentation exists: 🏁 Scripts executedThe following scripts were executed for the analysis: Script: #!/bin/bash
# Description: Look for documentation about the mailchimp-syncer program
# Expected: Find documentation in README or docs
# Search for documentation files
fd -t f "README|DOCUMENTATION|\.md$"
# Search for mailchimp-syncer mentions in documentation
rg -i "mailchimp.?syncer|marketing.?consent"
Length of output: 18085 |
||
]; | ||
|
||
let program_name = std::env::args().nth(1).unwrap_or_else(|| { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
ALTER TABLE courses DROP COLUMN ask_marketing_consent; | ||
DROP TABLE user_marketing_consents; | ||
DROP TABLE marketing_mailing_list_access_tokens; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
ALTER TABLE courses | ||
ADD COLUMN ask_marketing_consent BOOLEAN NOT NULL DEFAULT FALSE; | ||
|
||
COMMENT ON COLUMN courses.ask_marketing_consent IS 'Whether this course asks the user for marketing consent.'; | ||
|
||
CREATE TABLE user_marketing_consents ( | ||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, | ||
course_id UUID NOT NULL REFERENCES courses(id), | ||
course_language_group_id UUID NOT NULL REFERENCES course_language_groups(id), | ||
user_id UUID NOT NULL REFERENCES users(id), | ||
user_mailchimp_id VARCHAR(255), | ||
consent BOOLEAN NOT NULL, | ||
email_subscription_in_mailchimp VARCHAR(255), | ||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
deleted_at TIMESTAMP WITH TIME ZONE, | ||
synced_to_mailchimp_at TIMESTAMP WITH TIME ZONE, | ||
CONSTRAINT course_language_group_specific_marketing_user_uniqueness UNIQUE NULLS NOT DISTINCT(user_id, course_language_group_id) | ||
); | ||
|
||
CREATE TRIGGER set_timestamp BEFORE | ||
UPDATE ON user_marketing_consents FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); | ||
|
||
COMMENT ON TABLE user_marketing_consents IS 'This table is used to keep a record if a user has given a marketing consent to a course'; | ||
COMMENT ON COLUMN user_marketing_consents.id IS 'A unique, stable identifier for the record.'; | ||
COMMENT ON COLUMN user_marketing_consents.course_id IS 'Course that the user has access to.'; | ||
COMMENT ON COLUMN user_marketing_consents.course_language_group_id IS 'The course language group id that the mailing list is related to'; | ||
COMMENT ON COLUMN user_marketing_consents.user_id IS 'User who has the access to the course.'; | ||
COMMENT ON COLUMN user_marketing_consents.user_mailchimp_id IS 'Unique id for the user, provided by Mailchimp'; | ||
COMMENT ON COLUMN user_marketing_consents.consent IS 'Whether the user has given a marketing consent for a specific course.'; | ||
COMMENT ON COLUMN user_marketing_consents.email_subscription_in_mailchimp IS 'Tells the users email subscription status in Mailchimp'; | ||
COMMENT ON COLUMN user_marketing_consents.created_at IS 'Timestamp of when the record was created.'; | ||
COMMENT ON COLUMN user_marketing_consents.updated_at IS 'Timestamp when the record was last updated. The field is updated automatically by the set_timestamp trigger.'; | ||
COMMENT ON COLUMN user_marketing_consents.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; | ||
COMMENT ON COLUMN user_marketing_consents.synced_to_mailchimp_at IS 'Timestamp when the record was synced to mailchimp. If null, the record has not been synced.'; | ||
|
||
|
||
CREATE TABLE marketing_mailing_list_access_tokens ( | ||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, | ||
course_id UUID NOT NULL REFERENCES courses(id), | ||
course_language_group_id UUID NOT NULL REFERENCES course_language_groups(id), | ||
server_prefix VARCHAR(255) NOT NULL, | ||
access_token VARCHAR(255) NOT NULL, | ||
mailchimp_mailing_list_id VARCHAR(255) NOT NULL, | ||
nygrenh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), | ||
deleted_at TIMESTAMP WITH TIME ZONE | ||
); | ||
|
||
CREATE TRIGGER set_timestamp BEFORE | ||
UPDATE ON marketing_mailing_list_access_tokens FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); | ||
|
||
COMMENT ON TABLE marketing_mailing_list_access_tokens IS 'This table is used to keep a record of marketing mailing lists access tokens for each course language group'; | ||
COMMENT ON COLUMN marketing_mailing_list_access_tokens.id IS 'A unique, stable identifier for the record.'; | ||
COMMENT ON COLUMN marketing_mailing_list_access_tokens.course_id IS 'The course id that the the mailing list is related to'; | ||
COMMENT ON COLUMN marketing_mailing_list_access_tokens.course_language_group_id IS 'The course language group id that the mailing list is related to'; | ||
COMMENT ON COLUMN marketing_mailing_list_access_tokens.server_prefix IS 'This value is used to configure API requests to the correct Mailchimp server.'; | ||
COMMENT ON COLUMN marketing_mailing_list_access_tokens.access_token IS 'Token used for access authentication.'; | ||
COMMENT ON COLUMN marketing_mailing_list_access_tokens.mailchimp_mailing_list_id IS 'Id of the mailing list used for marketing in Mailchimp'; | ||
COMMENT ON COLUMN marketing_mailing_list_access_tokens.created_at IS 'Timestamp when the record was created.'; | ||
COMMENT ON COLUMN marketing_mailing_list_access_tokens.updated_at IS 'Timestamp when the record was last updated. The field is updated automatically by the set_timestamp trigger.'; | ||
COMMENT ON COLUMN marketing_mailing_list_access_tokens.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this you useQuery hook and after that useEffect if needed