mirror of
https://github.com/mastodon/mastodon.git
synced 2025-10-06 17:12:44 +00:00
create donation state
This commit is contained in:
parent
aa99edaf6d
commit
e22184c20e
72
app/javascript/mastodon/actions/donate.ts
Normal file
72
app/javascript/mastodon/actions/donate.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { apiGetDonateData } from '../api/donate';
|
||||||
|
import {
|
||||||
|
createAppThunk,
|
||||||
|
createDataLoadingThunk,
|
||||||
|
} from '../store/typed_functions';
|
||||||
|
|
||||||
|
import { focusCompose, resetCompose } from './compose';
|
||||||
|
import { closeModal, openModal } from './modal';
|
||||||
|
|
||||||
|
export const setDonateSeed = createAction<number>('donate/setSeed');
|
||||||
|
|
||||||
|
export const initializeDonate = createAppThunk(
|
||||||
|
(_arg, { dispatch, getState }) => {
|
||||||
|
if (!getState().donate.seed) {
|
||||||
|
let seed = Math.floor(Math.random() * 99) + 1;
|
||||||
|
try {
|
||||||
|
const storedSeed = localStorage.getItem('donate_seed');
|
||||||
|
if (storedSeed) {
|
||||||
|
seed = Number.parseInt(storedSeed, 10);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('donate_seed', seed.toString());
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// No local storage available, just set a seed for this session.
|
||||||
|
}
|
||||||
|
dispatch(setDonateSeed(seed));
|
||||||
|
}
|
||||||
|
void dispatch(fetchDonateData());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const fetchDonateData = createDataLoadingThunk(
|
||||||
|
'donate/fetch',
|
||||||
|
(_args: unknown, { getState }) => {
|
||||||
|
const state = getState();
|
||||||
|
return apiGetDonateData({
|
||||||
|
locale: state.meta.get('locale', 'en') as string,
|
||||||
|
seed: state.donate.seed ?? 1, // If we somehow don't have the seed, just set it to 1.
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(data) => data, // This is needed for TypeScript to infer the correct overload.
|
||||||
|
);
|
||||||
|
|
||||||
|
export const showDonateModal = createAppThunk(
|
||||||
|
(_arg, { dispatch, getState }) => {
|
||||||
|
const state = getState();
|
||||||
|
const lastPoll = state.donate.nextPoll;
|
||||||
|
if (!lastPoll || Date.now() >= lastPoll) {
|
||||||
|
void dispatch(fetchDonateData());
|
||||||
|
}
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'DONATE',
|
||||||
|
modalProps: {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const composeDonateShare = createAppThunk(
|
||||||
|
(_arg, { dispatch, getState }) => {
|
||||||
|
const state = getState();
|
||||||
|
const shareText = state.donate.apiResponse?.donation_success_post;
|
||||||
|
if (shareText) {
|
||||||
|
dispatch(resetCompose());
|
||||||
|
dispatch(focusCompose(shareText));
|
||||||
|
}
|
||||||
|
dispatch(closeModal({ modalType: 'DONATE', ignoreFocus: false }));
|
||||||
|
},
|
||||||
|
);
|
30
app/javascript/mastodon/api/donate.ts
Normal file
30
app/javascript/mastodon/api/donate.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import type {
|
||||||
|
DonateServerRequest,
|
||||||
|
DonateServerResponse,
|
||||||
|
} from '../api_types/donate';
|
||||||
|
|
||||||
|
// TODO: Proxy this through the backend.
|
||||||
|
const API_URL = 'https://api.joinmastodon.org/v1/donations/campaigns/active';
|
||||||
|
|
||||||
|
export const apiGetDonateData = async ({
|
||||||
|
locale,
|
||||||
|
seed,
|
||||||
|
}: DonateServerRequest) => {
|
||||||
|
// Create the URL with query parameters.
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
locale,
|
||||||
|
seed: seed.toString(),
|
||||||
|
platform: 'web',
|
||||||
|
source: 'menu',
|
||||||
|
});
|
||||||
|
const url = new URL(`${API_URL}?${params.toString()}`);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error fetching donation campaign: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
if (response.status === 204) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return response.json() as Promise<DonateServerResponse>;
|
||||||
|
};
|
23
app/javascript/mastodon/api_types/donate.ts
Normal file
23
app/javascript/mastodon/api_types/donate.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
export const donationFrequencyTypes = [
|
||||||
|
'one_time',
|
||||||
|
'monthly',
|
||||||
|
'yearly',
|
||||||
|
] as const;
|
||||||
|
export type DonationFrequency = (typeof donationFrequencyTypes)[number];
|
||||||
|
|
||||||
|
export interface DonateServerRequest {
|
||||||
|
locale: string;
|
||||||
|
seed: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DonateServerResponse {
|
||||||
|
id: string;
|
||||||
|
amounts: Record<DonationFrequency, Record<string, number[]>>;
|
||||||
|
donation_url: string;
|
||||||
|
banner_message: string;
|
||||||
|
banner_button_text: string;
|
||||||
|
donation_message: string;
|
||||||
|
donation_button_text: string;
|
||||||
|
donation_success_post: string;
|
||||||
|
default_currency: string;
|
||||||
|
}
|
|
@ -11,6 +11,7 @@ import { connect } from 'react-redux';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
||||||
|
import { initializeDonate } from '@/mastodon/actions/donate';
|
||||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
||||||
import { fetchNotifications } from 'mastodon/actions/notification_groups';
|
import { fetchNotifications } from 'mastodon/actions/notification_groups';
|
||||||
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
|
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
|
||||||
|
@ -394,6 +395,7 @@ class UI extends PureComponent {
|
||||||
this.props.dispatch(expandHomeTimeline());
|
this.props.dispatch(expandHomeTimeline());
|
||||||
this.props.dispatch(fetchNotifications());
|
this.props.dispatch(fetchNotifications());
|
||||||
this.props.dispatch(fetchServerTranslationLanguages());
|
this.props.dispatch(fetchServerTranslationLanguages());
|
||||||
|
this.props.dispatch(initializeDonate());
|
||||||
|
|
||||||
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
|
setTimeout(() => this.props.dispatch(fetchServer()), 3000);
|
||||||
}
|
}
|
||||||
|
|
36
app/javascript/mastodon/reducers/donate.ts
Normal file
36
app/javascript/mastodon/reducers/donate.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { createReducer } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { fetchDonateData, setDonateSeed } from '../actions/donate';
|
||||||
|
import type { DonateServerResponse } from '../api_types/donate';
|
||||||
|
|
||||||
|
interface DonateState {
|
||||||
|
apiResponse?: DonateServerResponse;
|
||||||
|
nextPoll?: number;
|
||||||
|
isFetching: boolean;
|
||||||
|
seed?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: DonateState = {
|
||||||
|
isFetching: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const donateReducer = createReducer(initialState, (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(setDonateSeed, (state, action) => {
|
||||||
|
state.seed = action.payload;
|
||||||
|
})
|
||||||
|
.addCase(fetchDonateData.pending, (state) => {
|
||||||
|
state.isFetching = true;
|
||||||
|
})
|
||||||
|
.addCase(fetchDonateData.rejected, (state) => {
|
||||||
|
state.isFetching = false;
|
||||||
|
})
|
||||||
|
.addCase(fetchDonateData.fulfilled, (state, action) => {
|
||||||
|
if (action.payload) {
|
||||||
|
state.apiResponse = action.payload;
|
||||||
|
}
|
||||||
|
// If we have data, poll in four hours, otherwise try again in one hour.
|
||||||
|
state.nextPoll = Date.now() + 1000 * 60 * 60 * (action.payload ? 4 : 1);
|
||||||
|
state.isFetching = false;
|
||||||
|
});
|
||||||
|
});
|
|
@ -12,6 +12,7 @@ import { composeReducer } from './compose';
|
||||||
import { contextsReducer } from './contexts';
|
import { contextsReducer } from './contexts';
|
||||||
import conversations from './conversations';
|
import conversations from './conversations';
|
||||||
import custom_emojis from './custom_emojis';
|
import custom_emojis from './custom_emojis';
|
||||||
|
import { donateReducer } from './donate';
|
||||||
import { dropdownMenuReducer } from './dropdown_menu';
|
import { dropdownMenuReducer } from './dropdown_menu';
|
||||||
import filters from './filters';
|
import filters from './filters';
|
||||||
import height_cache from './height_cache';
|
import height_cache from './height_cache';
|
||||||
|
@ -43,6 +44,7 @@ import user_lists from './user_lists';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
announcements,
|
announcements,
|
||||||
|
donate: donateReducer,
|
||||||
dropdownMenu: dropdownMenuReducer,
|
dropdownMenu: dropdownMenuReducer,
|
||||||
timelines,
|
timelines,
|
||||||
meta,
|
meta,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user