diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 46cb1ec4d77e..5bfc2c946cd2 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -24,7 +24,7 @@ inputs: DATADOG_SESSION_REPLAY_SAMPLE_RATE: description: 'Datadog session replay sample rate' required: false - DATADOG_SESSION_SAMPLE_RATE: + DATADOG_SESSION_SAMPLE_RATE: description: 'Datadog session sample rate' required: false GD_API_KEY: @@ -33,12 +33,18 @@ inputs: GD_APP_ID: description: 'Google drive app id' required: false - GD_CLIENT_ID: + GD_CLIENT_ID: description: 'Google drive client id' required: false RUDDERSTACK_KEY: description: 'Rudderstack key' required: false + GROWTHBOOK_CLIENT_KEY: + description: 'Growthbook key' + required: false + GROWTHBOOK_DECRYPTION_KEY: + description: 'Growthbook decryption key' + required: false DATADOG_SESSION_SAMPLE_RATE_LOGS: description: 'Datadog session sample rate for logs' required: false @@ -50,7 +56,7 @@ runs: env: NODE_ENV: ${{ inputs.NODE_ENV }} CROWDIN_WALLETS_API_KEY: ${{ inputs.CROWDIN_WALLETS_API_KEY }} - DATADOG_APPLICATION_ID: ${{ inputs.DATADOG_APPLICATION_ID }} + DATADOG_APPLICATION_ID: ${{ inputs.DATADOG_APPLICATION_ID }} DATADOG_CLIENT_TOKEN: ${{ inputs.DATADOG_CLIENT_TOKEN }} DATADOG_CLIENT_TOKEN_LOGS: ${{ inputs.DATADOG_CLIENT_TOKEN_LOGS }} DATADOG_SESSION_REPLAY_SAMPLE_RATE: ${{ inputs.DATADOG_SESSION_REPLAY_SAMPLE_RATE }} @@ -60,6 +66,8 @@ runs: GD_APP_ID: ${{ inputs.GD_APP_ID }} GD_CLIENT_ID: ${{ inputs.GD_CLIENT_ID }} RUDDERSTACK_KEY: ${{ inputs.RUDDERSTACK_KEY }} + GROWTHBOOK_CLIENT_KEY: ${{ inputs.GROWTHBOOK_CLIENT_KEY }} + GROWTHBOOK_DECRYPTION_KEY: ${{ inputs.GROWTHBOOK_DECRYPTION_KEY }} REF_NAME: ${{ inputs.REF_NAME }} run: npm run build:all shell: bash diff --git a/.github/workflows/release_production.yml b/.github/workflows/release_production.yml index c02367859b14..0130395e2757 100644 --- a/.github/workflows/release_production.yml +++ b/.github/workflows/release_production.yml @@ -21,7 +21,7 @@ jobs: uses: "./.github/actions/build" with: NODE_ENV: production - DATADOG_APPLICATION_ID: ${{ vars.DATADOG_APPLICATION_ID }} + DATADOG_APPLICATION_ID: ${{ vars.DATADOG_APPLICATION_ID }} DATADOG_CLIENT_TOKEN: ${{ vars.DATADOG_CLIENT_TOKEN }} DATADOG_CLIENT_TOKEN_LOGS: ${{ vars.DATADOG_CLIENT_TOKEN_LOGS }} DATADOG_SESSION_REPLAY_SAMPLE_RATE: ${{ vars.DATADOG_SESSION_REPLAY_SAMPLE_RATE }} @@ -31,6 +31,8 @@ jobs: GD_APP_ID: ${{ secrets.GD_APP_ID }} GD_CLIENT_ID: ${{ secrets.GD_CLIENT_ID }} RUDDERSTACK_KEY: ${{ vars.RUDDERSTACK_KEY }} + GROWTHBOOK_CLIENT_KEY: ${{ vars.GROWTHBOOK_CLIENT_KEY }} + GROWTHBOOK_DECRYPTION_KEY: ${{ vars.GROWTHBOOK_DECRYPTION_KEY }} REF_NAME: ${{ github.ref_name }} - name: Run tests run: npm test @@ -74,5 +76,5 @@ jobs: with: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} status: ${{ env.WORKFLOW_CONCLUSION }} - release_type: Production + release_type: Production version: ${{ needs.build_test_and_publish.outputs.RELEASE_VERSION}} diff --git a/.github/workflows/release_staging.yml b/.github/workflows/release_staging.yml index cdb0cc0d4395..fc84a82b0010 100644 --- a/.github/workflows/release_staging.yml +++ b/.github/workflows/release_staging.yml @@ -6,7 +6,7 @@ on: jobs: build_test_and_publish: - name: Build, Test and Publish to Cloudflare Staging + name: Build, Test and Publish to Cloudflare Staging runs-on: Runner_16cores_Deriv-app environment: Staging steps: @@ -21,7 +21,7 @@ jobs: with: NODE_ENV: staging CROWDIN_WALLETS_API_KEY: ${{ secrets.CROWDIN_WALLETS_API_KEY }} - DATADOG_APPLICATION_ID: ${{ vars.DATADOG_APPLICATION_ID }} + DATADOG_APPLICATION_ID: ${{ vars.DATADOG_APPLICATION_ID }} DATADOG_CLIENT_TOKEN: ${{ vars.DATADOG_CLIENT_TOKEN }} DATADOG_CLIENT_TOKEN_LOGS: ${{ vars.DATADOG_CLIENT_TOKEN_LOGS }} DATADOG_SESSION_REPLAY_SAMPLE_RATE: ${{ vars.DATADOG_SESSION_REPLAY_SAMPLE_RATE }} @@ -31,6 +31,8 @@ jobs: GD_APP_ID: ${{ secrets.GD_APP_ID }} GD_CLIENT_ID: ${{ secrets.GD_CLIENT_ID }} RUDDERSTACK_KEY: ${{ vars.RUDDERSTACK_KEY }} + GROWTHBOOK_CLIENT_KEY: ${{ vars.GROWTHBOOK_CLIENT_KEY }} + GROWTHBOOK_DECRYPTION_KEY: ${{ vars.GROWTHBOOK_DECRYPTION_KEY }} REF_NAME: ${{ github.ref_name }} - name: Run tests run: npm test @@ -38,9 +40,9 @@ jobs: uses: "./.github/actions/versioning" with: release_type: staging - - name: Publish to Cloudflare Pages Staging + - name: Publish to Cloudflare Pages Staging uses: "./.github/actions/publish_to_pages_staging" - with: + with: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.github/workflows/release_test.yml b/.github/workflows/release_test.yml index 53393c0f843a..aabe5b234724 100644 --- a/.github/workflows/release_test.yml +++ b/.github/workflows/release_test.yml @@ -6,7 +6,7 @@ on: jobs: build_test_and_publish: - name: Build, Test and Publish to Cloudflare Test + name: Build, Test and Publish to Cloudflare Test runs-on: Runner_8cores_Deriv-app environment: Staging steps: @@ -21,7 +21,7 @@ jobs: with: NODE_ENV: staging CROWDIN_WALLETS_API_KEY: ${{ secrets.CROWDIN_WALLETS_API_KEY }} - DATADOG_APPLICATION_ID: ${{ vars.DATADOG_APPLICATION_ID }} + DATADOG_APPLICATION_ID: ${{ vars.DATADOG_APPLICATION_ID }} DATADOG_CLIENT_TOKEN: ${{ vars.DATADOG_CLIENT_TOKEN }} DATADOG_CLIENT_TOKEN_LOGS: ${{ vars.DATADOG_CLIENT_TOKEN_LOGS }} DATADOG_SESSION_REPLAY_SAMPLE_RATE: ${{ vars.DATADOG_SESSION_REPLAY_SAMPLE_RATE }} @@ -31,11 +31,13 @@ jobs: GD_APP_ID: ${{ secrets.GD_APP_ID }} GD_CLIENT_ID: ${{ secrets.GD_CLIENT_ID }} RUDDERSTACK_KEY: ${{ vars.RUDDERSTACK_KEY }} + GROWTHBOOK_CLIENT_KEY: ${{ vars.GROWTHBOOK_CLIENT_KEY }} + GROWTHBOOK_DECRYPTION_KEY: ${{ vars.GROWTHBOOK_DECRYPTION_KEY }} REF_NAME: ${{ github.ref_name }} - name: Run tests run: npm test - - name: Publish to Cloudflare Pages Test + - name: Publish to Cloudflare Pages Test uses: "./.github/actions/publish_to_pages_test" - with: + with: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/.github/workflows/release_uat.yml b/.github/workflows/release_uat.yml index 5ffa6ed27d87..c3544404aa91 100644 --- a/.github/workflows/release_uat.yml +++ b/.github/workflows/release_uat.yml @@ -7,7 +7,7 @@ on: jobs: build_test_and_publish: - name: Build, Test and Publish to Cloudflare UAT + name: Build, Test and Publish to Cloudflare UAT runs-on: Runner_16cores_Deriv-app environment: Staging steps: @@ -21,7 +21,7 @@ jobs: uses: "./.github/actions/build" with: NODE_ENV: staging - DATADOG_APPLICATION_ID: ${{ vars.DATADOG_APPLICATION_ID }} + DATADOG_APPLICATION_ID: ${{ vars.DATADOG_APPLICATION_ID }} DATADOG_CLIENT_TOKEN: ${{ vars.DATADOG_CLIENT_TOKEN }} DATADOG_CLIENT_TOKEN_LOGS: ${{ vars.DATADOG_CLIENT_TOKEN_LOGS }} DATADOG_SESSION_REPLAY_SAMPLE_RATE: ${{ vars.DATADOG_SESSION_REPLAY_SAMPLE_RATE }} @@ -31,14 +31,16 @@ jobs: GD_APP_ID: ${{ secrets.GD_APP_ID }} GD_CLIENT_ID: ${{ secrets.GD_CLIENT_ID }} RUDDERSTACK_KEY: ${{ vars.RUDDERSTACK_KEY }} + GROWTHBOOK_CLIENT_KEY: ${{ vars.GROWTHBOOK_CLIENT_KEY }} + GROWTHBOOK_DECRYPTION_KEY: ${{ vars.GROWTHBOOK_DECRYPTION_KEY }} - name: Versioning uses: "./.github/actions/versioning" with: release_type: uat - name: Run tests run: npm test - - name: Publish to Cloudflare Pages UAT + - name: Publish to Cloudflare Pages UAT uses: "./.github/actions/publish_to_pages_uat" - with: + with: CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} diff --git a/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx b/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx index 8b2128711500..ef84f9ae0bfd 100644 --- a/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx +++ b/packages/core/src/App/Containers/AccountSignupModal/account-signup-modal.jsx @@ -3,8 +3,8 @@ import { Form, Formik } from 'formik'; import PropTypes from 'prop-types'; import { Button, Checkbox, Dialog, Loading, Text } from '@deriv/components'; -import { getLocation, SessionStore } from '@deriv/shared'; -import { localize } from '@deriv/translations'; +import { getLocation, SessionStore, shuffleArray } from '@deriv/shared'; +import { getLanguage, localize } from '@deriv/translations'; import { Analytics } from '@deriv/analytics'; import { WS } from 'Services'; @@ -12,6 +12,7 @@ import { observer, useStore } from '@deriv/stores'; import CitizenshipForm from '../CitizenshipModal/set-citizenship-form.jsx'; import PasswordSelectionModal from '../PasswordSelectionModal/password-selection-modal.jsx'; +import QuestionnaireModal from '../QuestionnaireModal'; import ResidenceForm from '../SetResidenceModal/set-residence-form.jsx'; import validateSignupFields from './validate-signup-fields.jsx'; @@ -35,6 +36,10 @@ const AccountSignup = ({ const [pw_input, setPWInput] = React.useState(''); const [is_password_modal, setIsPasswordModal] = React.useState(false); const [is_disclaimer_accepted, setIsDisclaimerAccepted] = React.useState(false); + const [is_questionnaire, setIsQuestionnaire] = React.useState(false); + const [ab_questionnaire, setABQuestionnaire] = React.useState(); + const [modded_state, setModdedState] = React.useState({}); + const language = getLanguage(); const checkResidenceIsBrazil = selected_country => selected_country && residence_list[indexOfSelection(selected_country)]?.value?.toLowerCase() === 'br'; @@ -58,6 +63,23 @@ const AccountSignup = ({ } setIsLoading(false); }); + // need to modify data from ab testing platform to reach translation and tracking needs + const fetchQuestionnarieData = () => { + let ab_value = Analytics.getFeatureValue('questionnaire-config', 'inactive'); + const default_ab_value = ab_value; + ab_value = ab_value?.[language] ?? ab_value?.EN ?? ab_value; + if (ab_value?.show_answers_in_random_order) { + ab_value = [ + { ...default_ab_value.default }, + { + ...ab_value, + answers: shuffleArray(ab_value.answers), + }, + ]; + } else if (ab_value !== 'inactive') ab_value = [{ ...default_ab_value.default }, { ...ab_value }]; + return ab_value; + }; + setABQuestionnaire(fetchQuestionnarieData()); Analytics.trackEvent('ce_virtual_signup_form', { action: 'signup_confirmed', @@ -75,6 +97,8 @@ const AccountSignup = ({ const indexOfSelection = selected_country => residence_list.findIndex(item => item.text.toLowerCase() === selected_country?.toLowerCase()); + const handleSignup = () => onSignup(modded_state, onSignupComplete); + const onSignupPassthrough = values => { const index_of_selected_residence = indexOfSelection(values.residence); const index_of_selected_citizenship = indexOfSelection(values.citizenship); @@ -84,8 +108,12 @@ const AccountSignup = ({ residence: residence_list[index_of_selected_residence].value, citizenship: residence_list[index_of_selected_citizenship].value, }; + setModdedState(modded_values); - onSignup(modded_values, onSignupComplete); + // a/b test + ab_questionnaire === 'inactive' + ? onSignup(modded_values, onSignupComplete) + : setIsQuestionnaire(!!ab_questionnaire); }; const onSignupComplete = error => { @@ -188,19 +216,28 @@ const AccountSignup = ({ ) : ( - + + {is_questionnaire ? ( + + ) : ( + + )} + )} )} diff --git a/packages/core/src/App/Containers/QuestionnaireModal/__tests__/questionnaire-modal.spec.tsx b/packages/core/src/App/Containers/QuestionnaireModal/__tests__/questionnaire-modal.spec.tsx new file mode 100644 index 000000000000..b77ce55c11f9 --- /dev/null +++ b/packages/core/src/App/Containers/QuestionnaireModal/__tests__/questionnaire-modal.spec.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { render, screen } from '@testing-library/react'; +import QuestionnaireModal from '../questionnaire-modal'; +import userEvent from '@testing-library/user-event'; +import crypto from 'crypto'; + +describe('QuestionnaireModal', () => { + const mock_props = { + ab_questionnaire: [ + { + id: '1', + question: 'Default question', + show_answers_in_random_order: true, + answers: [ + { code: 'a', text: 'Default A' }, + { code: 'b', text: 'Default B', header: 'Default Header B' }, + ], + }, + { + id: '1', + question: 'Sample question', + show_answers_in_random_order: false, + answers: [ + { code: 'a', text: 'Option A' }, + { code: 'b', text: 'Option B', header: 'Header B' }, + ], + }, + ], + handleSignup: jest.fn(), + }; + + let modal_root_el: HTMLDivElement; + + beforeAll(() => { + modal_root_el = document.createElement('div'); + modal_root_el.setAttribute('id', 'modal_root'); + document.body.appendChild(modal_root_el); + + Object.defineProperty(global, 'crypto', { + value: { + getRandomValues: (arr: Uint32Array) => crypto.randomBytes(arr.length), + }, + }); + }); + + afterAll(() => { + document.body.removeChild(modal_root_el); + }); + + it('renders QuestionnaireModal component correctly', () => { + render( + + + + ); + expect(screen.getByText('Sample question')).toBeInTheDocument(); + }); + + it('calls handleSignup when an answer is clicked', () => { + render( + + + + ); + const option_a = screen.getByTestId('a_questionnaire'); + userEvent.click(option_a); + expect(mock_props.handleSignup).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/core/src/App/Containers/QuestionnaireModal/index.ts b/packages/core/src/App/Containers/QuestionnaireModal/index.ts new file mode 100644 index 000000000000..8cb6ec8d1c3f --- /dev/null +++ b/packages/core/src/App/Containers/QuestionnaireModal/index.ts @@ -0,0 +1,3 @@ +import QuestionnaireModal from './questionnaire-modal'; + +export default QuestionnaireModal; diff --git a/packages/core/src/App/Containers/QuestionnaireModal/questionnaire-modal.scss b/packages/core/src/App/Containers/QuestionnaireModal/questionnaire-modal.scss new file mode 100644 index 000000000000..7a555ee453c3 --- /dev/null +++ b/packages/core/src/App/Containers/QuestionnaireModal/questionnaire-modal.scss @@ -0,0 +1,66 @@ +.questionnaire-modal { + border-radius: 1.6rem; + + @include mobile { + border-radius: 1.6rem; + padding-inline: 0; + } + .dc-btn { + height: unset; + padding: unset; + } + .dc-modal__container_questionnaire-modal { + padding-block: 3.8rem; + } + &__answers { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 1.6rem; + padding-block-start: 3.2rem; + + &_content { + display: flex; + justify-content: space-between; + inline-size: 32.8rem; + padding-block: 2rem; + padding-inline: 1.6rem; + box-shadow: 0 0.4rem 0.6rem -0.2rem rgba(14, 14, 14, 0.03), 0 1.2rem 1.6rem -0.4rem rgba(14, 14, 14, 0.08); + border-radius: $BORDER-RADIUS * 4; + border: 0.1rem solid var(--general-active); + @include mobile { + inline-size: 28rem; + } + &:hover { + box-shadow: 0 0.2rem 0.8rem 0 var(--shadow-menu); + } + } + } + &__options { + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; + gap: 1.6rem; + padding-block-start: 3.2rem; + + &_card { + display: flex; + justify-content: center; + align-items: center; + min-inline-size: 15rem; + min-block-size: 13.8rem; + box-shadow: 0 0.4rem 0.6rem -0.2rem rgba(14, 14, 14, 0.03), 0 1.2rem 1.6rem -0.4rem rgba(14, 14, 14, 0.08); + border-radius: $BORDER-RADIUS * 4; + border: 0.1rem solid var(--general-active); + &:hover { + box-shadow: 0 0.2rem 0.8rem 0 var(--shadow-menu); + } + @include mobile { + min-inline-size: 13.2rem; + min-block-size: 13.8rem; + } + } + } +} diff --git a/packages/core/src/App/Containers/QuestionnaireModal/questionnaire-modal.tsx b/packages/core/src/App/Containers/QuestionnaireModal/questionnaire-modal.tsx new file mode 100644 index 000000000000..08ea3358b108 --- /dev/null +++ b/packages/core/src/App/Containers/QuestionnaireModal/questionnaire-modal.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import classNames from 'classnames'; +import { Analytics } from '@deriv/analytics'; +import { Button, Text } from '@deriv/components'; +import './questionnaire-modal.scss'; + +type TAnswers = { + code: string; + text: string; + header?: string; +}; +type TABQuestionnaire = { + id: string; + question: string; + show_answers_in_random_order: boolean; + answers: TAnswers[]; +}; +export type TQuestionnaireModal = { + ab_questionnaire: TABQuestionnaire[]; + handleSignup: (...args: any) => void; +}; + +const QuestionnaireModal = ({ ab_questionnaire, handleSignup }: TQuestionnaireModal) => { + const a_variant = !!ab_questionnaire[1].answers?.[0]?.header; + React.useEffect(() => { + Analytics.trackEvent('ce_questionnaire_form', { + action: 'open', + }); + }, [ab_questionnaire]); + + const onClickAnswer = (answer_code: string, answer_index: number) => { + Analytics.trackEvent('ce_questionnaire_form', { + action: 'choose_answer', + question: ab_questionnaire[0].question, + answer_content: ab_questionnaire[0].answers.filter(({ code }) => code === answer_code)[0].text, + answer_code, + answer_index, + }); + handleSignup(); + }; + + return ( +
+ + {ab_questionnaire[1].question} + + +
+ ); +}; + +export default QuestionnaireModal;