-
-
Notifications
You must be signed in to change notification settings - Fork 125
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
Add insiders settings #7151
Draft
dem4ron
wants to merge
23
commits into
main
Choose a base branch
from
hide-ad-pref
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Add insiders settings #7151
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
b68c664
Add InsiderBenefitsForm
dem4ron 82aef21
Add component
dem4ron af05902
Add migration
dem4ron 07bd2d8
Progress
dem4ron d04fb71
Sort things out better
dem4ron 564408d
Permit editing hide thingy
dem4ron fed021d
Pass down props
dem4ron 3ed54b3
Disable form for non insiders
dem4ron b7b64e7
Remove unused import
dem4ron 63ea5b0
Create an insiders settings page
dem4ron 59c44b9
Add new components
dem4ron 53b5bc6
Add API endpoints
dem4ron 0ec75f6
Improve code generation services
iHiD 642f9b2
Simplify things
dem4ron affb637
Refine things
dem4ron 668f8a4
Finalise affiliate form
dem4ron af1908b
Rename everything, add free coupon form
dem4ron 2487590
Add rendering tests
dem4ron 6613d9b
Add a WIP generate test
dem4ron 02ca49e
Stub affiliate generation in test
dem4ron 278988d
Restore schema
dem4ron 8ebfded
Fix missing cols from schema
dem4ron 467b612
Refine user changes hide adverts test
dem4ron File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
15 changes: 15 additions & 0 deletions
15
app/controllers/api/generate_bootcamp_affiliate_coupon_code.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
class API::GenerateBootcampAffiliateCouponCode < API::BaseController | ||
skip_before_action :authenticate_user! | ||
before_action :authenticate_user | ||
|
||
def create | ||
return unless current_user.insider? | ||
return if current_user.bootcamp_affiliate_coupon_code.present? | ||
|
||
User::GenerateBootcampAffiliateCouponCode.(current_user) | ||
|
||
render json: { coupon_code: current_user.bootcamp_affiliate_coupon_code } | ||
rescue StandardError => e | ||
render json: { error: e.message }, status: :unprocessable_entity | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
class API::GenerateBootcampFreeCouponCode < API::BaseController | ||
skip_before_action :authenticate_user! | ||
before_action :authenticate_user | ||
|
||
def create | ||
return if current_user.insiders_status != :active_lifetime | ||
return if current_user.bootcamp_free_coupon_code.present? | ||
|
||
User::GenerateBootcampFreeCouponCode.(current_user) | ||
|
||
render json: { coupon_code: current_user.bootcamp_free_coupon_code } | ||
rescue StandardError => e | ||
render json: { error: e.message }, status: :unprocessable_entity | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
app/helpers/react_components/settings/bootcamp_affiliate_form.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
module ReactComponents | ||
module Settings | ||
class BootcampAffiliateForm < ReactComponent | ||
def to_s | ||
super("settings-bootcamp-affiliate-form", { | ||
preferences:, | ||
insiders_status: current_user.insiders_status, | ||
links: { | ||
update: Exercism::Routes.api_settings_user_preferences_url, | ||
insiders_path: Exercism::Routes.insiders_path | ||
} | ||
}) | ||
end | ||
|
||
def preferences = current_user.preferences.slice(:hide_website_adverts) | ||
end | ||
end | ||
end |
18 changes: 18 additions & 0 deletions
18
app/helpers/react_components/settings/insider_benefits_form.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
module ReactComponents | ||
module Settings | ||
class InsiderBenefitsForm < ReactComponent | ||
def to_s | ||
super("settings-insider-benefits-form", { | ||
preferences:, | ||
insiders_status: current_user.insiders_status, | ||
links: { | ||
update: Exercism::Routes.api_settings_user_preferences_url, | ||
insiders_path: Exercism::Routes.insiders_path | ||
} | ||
}) | ||
end | ||
|
||
def preferences = current_user.preferences.slice(:hide_website_adverts) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
113 changes: 113 additions & 0 deletions
113
app/javascript/components/settings/BootcampAffiliateForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import React, { useState, useCallback } from 'react' | ||
import { Icon, GraphicalIcon } from '@/components/common' | ||
import { FormButton } from '@/components/common/FormButton' | ||
import { FormMessage } from './FormMessage' | ||
import { useMutation } from '@tanstack/react-query' | ||
import { sendRequest } from '@/utils/send-request' | ||
|
||
type Links = { | ||
update: string | ||
insidersPath: string | ||
} | ||
|
||
export type UserPreferences = { | ||
hideWebsiteAdverts: boolean | ||
} | ||
|
||
const DEFAULT_ERROR = new Error('Unable to change preferences') | ||
|
||
export default function BootcampAffiliateForm({ | ||
defaultPreferences, | ||
insidersStatus, | ||
links, | ||
}: { | ||
defaultPreferences: UserPreferences | ||
insidersStatus: string | ||
links: Links | ||
}): JSX.Element { | ||
const [hideAdverts, setHideAdverts] = useState( | ||
defaultPreferences.hideWebsiteAdverts | ||
) | ||
|
||
const { | ||
mutate: mutation, | ||
status, | ||
error, | ||
} = useMutation(async () => { | ||
const { fetch } = sendRequest({ | ||
endpoint: links.update, | ||
method: 'PATCH', | ||
body: JSON.stringify({ | ||
user_preferences: { hide_website_adverts: hideAdverts }, | ||
}), | ||
}) | ||
|
||
return fetch | ||
}) | ||
|
||
const handleSubmit = useCallback( | ||
(e) => { | ||
e.preventDefault() | ||
|
||
mutation() | ||
}, | ||
[mutation] | ||
) | ||
|
||
const isInsider = | ||
insidersStatus == 'active' || insidersStatus == 'active_lifetime' | ||
|
||
return ( | ||
<form data-turbo="false" onSubmit={handleSubmit}> | ||
<h2>Bootcamp Affiliate</h2> | ||
<InfoMessage | ||
isInsider={isInsider} | ||
insidersStatus={insidersStatus} | ||
insidersPath={links.insidersPath} | ||
/> | ||
<button className="btn btn-primary">Click to generate code</button> | ||
</form> | ||
) | ||
} | ||
|
||
export function InfoMessage({ | ||
insidersStatus, | ||
insidersPath, | ||
isInsider, | ||
}: { | ||
insidersStatus: string | ||
insidersPath: string | ||
isInsider: boolean | ||
}): JSX.Element { | ||
if (isInsider) { | ||
return ( | ||
<p className="text-p-base mb-16"> | ||
You've not yet generated your affiliate code. | ||
</p> | ||
) | ||
} | ||
|
||
switch (insidersStatus) { | ||
case 'eligible': | ||
case 'eligible_lifetime': | ||
return ( | ||
<p className="text-p-base mb-16"> | ||
You're eligible to join Insiders.{' '} | ||
<a href={insidersPath}>Get started here.</a> | ||
</p> | ||
) | ||
default: | ||
return ( | ||
<p className="text-p-base mb-16"> | ||
These are exclusive options for Exercism Insiders. | ||
<strong> | ||
<a className="text-prominentLinkColor" href={insidersPath}> | ||
Donate to Exercism | ||
</a> | ||
</strong>{' '} | ||
and become an Insider to access these benefits with Dark Mode, ChatGPT | ||
integration and more. | ||
</p> | ||
) | ||
} | ||
} |
151 changes: 151 additions & 0 deletions
151
app/javascript/components/settings/InsiderBenefitsForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import React, { useState, useCallback } from 'react' | ||
import { Icon, GraphicalIcon } from '@/components/common' | ||
import { FormButton } from '@/components/common/FormButton' | ||
import { FormMessage } from './FormMessage' | ||
import { useMutation } from '@tanstack/react-query' | ||
import { sendRequest } from '@/utils/send-request' | ||
|
||
type Links = { | ||
update: string | ||
insidersPath: string | ||
} | ||
|
||
export type UserPreferences = { | ||
hideWebsiteAdverts: boolean | ||
} | ||
|
||
const DEFAULT_ERROR = new Error('Unable to change preferences') | ||
|
||
export default function InsiderBenefitsForm({ | ||
defaultPreferences, | ||
insidersStatus, | ||
links, | ||
}: { | ||
defaultPreferences: UserPreferences | ||
insidersStatus: string | ||
links: Links | ||
}): JSX.Element { | ||
const [hideAdverts, setHideAdverts] = useState( | ||
defaultPreferences.hideWebsiteAdverts | ||
) | ||
|
||
const { | ||
mutate: mutation, | ||
status, | ||
error, | ||
} = useMutation(async () => { | ||
const { fetch } = sendRequest({ | ||
endpoint: links.update, | ||
method: 'PATCH', | ||
body: JSON.stringify({ | ||
user_preferences: { hide_website_adverts: hideAdverts }, | ||
}), | ||
}) | ||
|
||
return fetch | ||
}) | ||
|
||
const handleSubmit = useCallback( | ||
(e) => { | ||
e.preventDefault() | ||
|
||
mutation() | ||
}, | ||
[mutation] | ||
) | ||
|
||
const isInsider = | ||
insidersStatus == 'active' || insidersStatus == 'active_lifetime' | ||
|
||
return ( | ||
<form data-turbo="false" onSubmit={handleSubmit}> | ||
<h2>Insider benefits</h2> | ||
<InfoMessage | ||
isInsider={isInsider} | ||
insidersStatus={insidersStatus} | ||
insidersPath={links.insidersPath} | ||
/> | ||
<label className="c-checkbox-wrapper"> | ||
<input | ||
type="checkbox" | ||
disabled={!isInsider} | ||
checked={hideAdverts} | ||
onChange={(e) => setHideAdverts(e.target.checked)} | ||
/> | ||
<div className="row"> | ||
<div className="c-checkbox"> | ||
<GraphicalIcon icon="checkmark" /> | ||
</div> | ||
Hide website adverts | ||
</div> | ||
</label> | ||
<div className="form-footer"> | ||
<FormButton | ||
disabled={!isInsider} | ||
status={status} | ||
className="btn-primary btn-m" | ||
> | ||
Change preferences | ||
</FormButton> | ||
<FormMessage | ||
status={status} | ||
defaultError={DEFAULT_ERROR} | ||
error={error} | ||
SuccessMessage={SuccessMessage} | ||
/> | ||
</div> | ||
</form> | ||
) | ||
} | ||
|
||
const SuccessMessage = () => { | ||
return ( | ||
<div className="status success"> | ||
<Icon icon="completed-check-circle" alt="Success" /> | ||
Your preferences have been updated | ||
</div> | ||
) | ||
} | ||
|
||
export function InfoMessage({ | ||
insidersStatus, | ||
insidersPath, | ||
isInsider, | ||
}: { | ||
insidersStatus: string | ||
insidersPath: string | ||
isInsider: boolean | ||
}): JSX.Element { | ||
if (isInsider) { | ||
return ( | ||
<p className="text-p-base mb-16"> | ||
These are exclusive options to enhance your experience as an Exercism | ||
Insdier | ||
</p> | ||
) | ||
} | ||
|
||
switch (insidersStatus) { | ||
case 'eligible': | ||
case 'eligible_lifetime': | ||
return ( | ||
<p className="text-p-base mb-16"> | ||
You're eligible to join Insiders.{' '} | ||
<a href={insidersPath}>Get started here.</a> | ||
</p> | ||
) | ||
default: | ||
return ( | ||
<p className="text-p-base mb-16"> | ||
These are exclusive options for Exercism Insiders. | ||
<strong> | ||
<a className="text-prominentLinkColor" href={insidersPath}> | ||
Donate to Exercism | ||
</a> | ||
</strong>{' '} | ||
and become an Insider to access these benefits with Dark Mode, ChatGPT | ||
integration and more. | ||
</p> | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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 these are better places into the command below (the one is already there and I'll add the insiders guard too). I prefer keeping all the logic in one place.
Also, you can't just "return" here as you need to render json, so it should be
return render(json: {})
to be a valid response for a JSON API. You could also raise an error of some description likeraise "Not insider"
which would get caught by your block, but as we have standardised error messages in the API, that's probably not ideal.So I would make this method:
or if you want to raise an error if the code is missing:
And then you need to add translations for
could_not_generate_coupon_code
(seeconfig/locales/api/en.yml
)