mirror of
https://github.com/lux-tel/session-desktop.git
synced 2026-10-30 20:33:24 +00:00
324 lines
11 KiB
TypeScript
324 lines
11 KiB
TypeScript
import { useSelector } from 'react-redux';
|
|
import { ProOriginatingPlatform } from 'libsession_util_nodejs';
|
|
import { zodSafeParse } from '../../util/zod';
|
|
import type { StateType } from '../reducer';
|
|
import {
|
|
proBackendDataActions,
|
|
RequestActionArgs,
|
|
WithCallerContext,
|
|
type ProBackendDataState,
|
|
} from '../ducks/proBackendData';
|
|
import { SettingsKey } from '../../data/settings-key';
|
|
import { Storage } from '../../util/storage';
|
|
import { ProDetailsResultSchema } from '../../session/apis/pro_backend_api/schemas';
|
|
import {
|
|
getDataFeatureFlag,
|
|
getFeatureFlag,
|
|
getFeatureFlagMemo,
|
|
MockProAccessExpiryOptions,
|
|
} from '../ducks/types/releasedFeaturesReduxTypes';
|
|
import { NetworkTime } from '../../util/NetworkTime';
|
|
import {
|
|
formatDateWithLocale,
|
|
formatRoundedUpTimeUntilTimestamp,
|
|
} from '../../util/i18n/formatting/generics';
|
|
import {
|
|
getProOriginatingPlatformFromProPaymentProvider,
|
|
ProAccessVariant,
|
|
ProPaymentProvider,
|
|
ProStatus,
|
|
} from '../../session/apis/pro_backend_api/types';
|
|
import LIBSESSION_CONSTANTS from '../../session/utils/libsession/libsession_constants';
|
|
import { getAppDispatch } from '../dispatch';
|
|
import { sleepFor } from '../../session/utils/Promise';
|
|
import { assertUnreachable } from '../../types/sqlSharedTypes';
|
|
|
|
const getProBackendData = (state: StateType): ProBackendDataState => {
|
|
return state.proBackendData;
|
|
};
|
|
|
|
function getProDetailsFromStorage() {
|
|
const response = Storage.get(SettingsKey.proDetails);
|
|
if (!response) {
|
|
return null;
|
|
}
|
|
const result = zodSafeParse(ProDetailsResultSchema, response);
|
|
if (result.success) {
|
|
return result.data;
|
|
}
|
|
void Storage.remove(SettingsKey.proDetails);
|
|
window?.log?.error(
|
|
'failed to parse pro details from storage, removing item. error:',
|
|
result.error
|
|
);
|
|
return null;
|
|
}
|
|
|
|
export function proAccessVariantToString(variant: ProAccessVariant): string {
|
|
switch (variant) {
|
|
case ProAccessVariant.OneMonth:
|
|
return '1 Month';
|
|
case ProAccessVariant.ThreeMonth:
|
|
return '3 Months';
|
|
case ProAccessVariant.TwelveMonth:
|
|
return '12 Months';
|
|
case ProAccessVariant.Nil:
|
|
return 'N/A';
|
|
default:
|
|
return assertUnreachable(variant, `Unknown pro access variant: ${variant}`);
|
|
}
|
|
}
|
|
|
|
export function getProProviderConstantsWithFallbacks(provider: ProPaymentProvider) {
|
|
const libsessionPaymentProvider = getProOriginatingPlatformFromProPaymentProvider(provider);
|
|
const constants = LIBSESSION_CONSTANTS.LIBSESSION_PRO_PROVIDERS[libsessionPaymentProvider];
|
|
|
|
if (!constants.store) {
|
|
constants.store = LIBSESSION_CONSTANTS.LIBSESSION_PRO_PROVIDERS.Google.store;
|
|
}
|
|
|
|
if (!constants.store_other) {
|
|
constants.store_other = LIBSESSION_CONSTANTS.LIBSESSION_PRO_PROVIDERS.Google.store_other;
|
|
}
|
|
|
|
return constants;
|
|
}
|
|
|
|
function getMockedProAccessExpiry(variant: MockProAccessExpiryOptions): number | null {
|
|
switch (variant) {
|
|
case MockProAccessExpiryOptions.P7D:
|
|
return 7 * 24 * 60 * 60 * 1000;
|
|
case MockProAccessExpiryOptions.P29D:
|
|
return 29 * 24 * 60 * 60 * 1000;
|
|
case MockProAccessExpiryOptions.P30D:
|
|
return 30 * 24 * 60 * 60 * 1000;
|
|
case MockProAccessExpiryOptions.P30DT1S:
|
|
return 30 * 24 * 60 * 61 * 1000;
|
|
case MockProAccessExpiryOptions.P90D:
|
|
return 90 * 24 * 60 * 60 * 1000;
|
|
case MockProAccessExpiryOptions.P300D:
|
|
return 300 * 24 * 60 * 60 * 1000;
|
|
case MockProAccessExpiryOptions.P365D:
|
|
return 365 * 24 * 60 * 60 * 1000;
|
|
case MockProAccessExpiryOptions.P24DT1M:
|
|
return 24 * 24 * 60 * 60 * 1000 + 60 * 60 * 1000;
|
|
case MockProAccessExpiryOptions.PT24H1M:
|
|
return 24 * 60 * 60 * 1000 + 60 * 60 * 1000;
|
|
case MockProAccessExpiryOptions.PT23H59M:
|
|
return 23 * 60 * 60 * 1000 + 59 * 60 * 1000;
|
|
case MockProAccessExpiryOptions.PT33M:
|
|
return 33 * 60 * 1000;
|
|
case MockProAccessExpiryOptions.PT1M:
|
|
return 1 * 60 * 1000;
|
|
case MockProAccessExpiryOptions.PT10S:
|
|
return 10 * 1000;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
type ProAccessDetailsSourceData = {
|
|
currentStatus: ProStatus;
|
|
autoRenew: boolean;
|
|
inGracePeriod: boolean;
|
|
variant: ProAccessVariant;
|
|
expiryTimeMs: number;
|
|
isPlatformRefundAvailable: boolean;
|
|
provider: ProPaymentProvider;
|
|
isLoading: boolean;
|
|
isError: boolean;
|
|
};
|
|
|
|
type ProAccessDetails = {
|
|
currentStatus: ProStatus;
|
|
autoRenew: boolean;
|
|
inGracePeriod: boolean;
|
|
variant: ProAccessVariant;
|
|
variantString: string;
|
|
expiryTimeMs: number;
|
|
expiryTimeDateString: string;
|
|
expiryTimeRelativeString: string;
|
|
isPlatformRefundAvailable: boolean;
|
|
provider: ProPaymentProvider;
|
|
providerConstants: (typeof LIBSESSION_CONSTANTS)['LIBSESSION_PRO_PROVIDERS'][ProOriginatingPlatform];
|
|
};
|
|
|
|
// These values are used if pro isnt available or if no data is available from the backend.
|
|
export const defaultProAccessDetailsSourceData = {
|
|
currentStatus: ProStatus.NeverBeenPro,
|
|
autoRenew: true,
|
|
inGracePeriod: false,
|
|
variant: ProAccessVariant.Nil,
|
|
expiryTimeMs: 0,
|
|
isPlatformRefundAvailable: false,
|
|
provider: ProPaymentProvider.Nil,
|
|
isLoading: false,
|
|
isError: false,
|
|
} satisfies ProAccessDetailsSourceData;
|
|
|
|
export type ProcessedProDetails = {
|
|
isLoading: boolean;
|
|
isFetching: boolean;
|
|
isError: boolean;
|
|
data: ProAccessDetails;
|
|
t: number;
|
|
};
|
|
|
|
function processProBackendData({
|
|
isLoading: _isLoading,
|
|
isFetching: _isFetching,
|
|
isError: _isError,
|
|
data,
|
|
t,
|
|
}: ProBackendDataState['details']): ProcessedProDetails {
|
|
const mockIsLoading = getFeatureFlag('mockProBackendLoading');
|
|
const mockIsError = getFeatureFlag('mockProBackendError');
|
|
|
|
const mockVariant = getDataFeatureFlag('mockProAccessVariant');
|
|
const mockPlatform = getDataFeatureFlag('mockProPaymentProvider');
|
|
const mockCancelled = getFeatureFlag('mockCurrentUserHasProCancelled');
|
|
const mockInGracePeriod = getFeatureFlag('mockCurrentUserHasProInGracePeriod');
|
|
const mockIsPlatformRefundAvailable = !getFeatureFlag(
|
|
'mockCurrentUserHasProPlatformRefundExpired'
|
|
);
|
|
const expiryVariant = getDataFeatureFlag('mockProAccessExpiry');
|
|
const mockedExpiryDuration =
|
|
expiryVariant !== null ? getMockedProAccessExpiry(expiryVariant) : null;
|
|
let mockExpiry = null;
|
|
if (mockedExpiryDuration !== null) {
|
|
// NOTE: the mock expiry time should be pinned to x - 250ms after "now", the -250ms ensures the string
|
|
// representation rounds up to the expected mock value and prevents render lag from changing the timestamp
|
|
const now = Date.now() - 250;
|
|
mockExpiry = now + mockedExpiryDuration;
|
|
}
|
|
|
|
const isLoading = mockIsLoading || _isLoading;
|
|
const isFetching = mockIsLoading || _isFetching;
|
|
const isError = mockIsLoading ? false : mockIsError || _isError;
|
|
|
|
const now = NetworkTime.now();
|
|
|
|
const expiryTimeMs =
|
|
mockExpiry ?? data?.expiry_unix_ts_ms ?? defaultProAccessDetailsSourceData.expiryTimeMs;
|
|
|
|
const latestAccess = data?.items?.[0];
|
|
const provider =
|
|
mockPlatform ?? latestAccess?.payment_provider ?? defaultProAccessDetailsSourceData.provider;
|
|
const variant = mockVariant ?? latestAccess?.plan ?? defaultProAccessDetailsSourceData.variant;
|
|
const isPlatformRefundAvailable =
|
|
mockIsPlatformRefundAvailable ||
|
|
(latestAccess?.platform_refund_expiry_unix_ts_ms &&
|
|
now < latestAccess.platform_refund_expiry_unix_ts_ms) ||
|
|
defaultProAccessDetailsSourceData.isPlatformRefundAvailable;
|
|
|
|
const autoRenew = mockCancelled
|
|
? !mockCancelled
|
|
: (data?.auto_renewing ?? defaultProAccessDetailsSourceData.autoRenew);
|
|
|
|
let beginAutoRenew = 0;
|
|
if (data) {
|
|
beginAutoRenew = data.expiry_unix_ts_ms - data.grace_period_duration_ms;
|
|
}
|
|
|
|
let inGracePeriod = mockInGracePeriod;
|
|
if (beginAutoRenew && !mockInGracePeriod) {
|
|
inGracePeriod = autoRenew && now >= beginAutoRenew && now < expiryTimeMs;
|
|
}
|
|
|
|
return {
|
|
data: {
|
|
currentStatus: data?.status ?? defaultProAccessDetailsSourceData.currentStatus,
|
|
autoRenew,
|
|
inGracePeriod,
|
|
variant,
|
|
variantString: proAccessVariantToString(variant),
|
|
expiryTimeMs,
|
|
expiryTimeDateString: formatDateWithLocale({
|
|
date: new Date(beginAutoRenew),
|
|
formatStr: 'MMM d, yyyy',
|
|
}),
|
|
expiryTimeRelativeString: formatRoundedUpTimeUntilTimestamp(beginAutoRenew),
|
|
isPlatformRefundAvailable,
|
|
provider,
|
|
providerConstants: getProProviderConstantsWithFallbacks(provider),
|
|
},
|
|
isLoading,
|
|
isFetching,
|
|
isError,
|
|
t,
|
|
};
|
|
}
|
|
|
|
export const getProBackendProDetails = (state: StateType): ProcessedProDetails => {
|
|
const details = getProBackendData(state).details;
|
|
const mergedDetails = details.data ? details : { ...details, data: getProDetailsFromStorage() };
|
|
|
|
return processProBackendData(mergedDetails);
|
|
};
|
|
|
|
export const getProBackendCurrentUserStatus = (state: StateType) => {
|
|
return getProBackendProDetails(state).data?.currentStatus;
|
|
};
|
|
|
|
export const useProBackendProDetails = () => {
|
|
return useSelector(getProBackendProDetails);
|
|
};
|
|
|
|
export const useProBackendCurrentUserStatus = () => {
|
|
return useSelector(getProBackendCurrentUserStatus);
|
|
};
|
|
|
|
export function useProBackendRefetch() {
|
|
const dispatch = getAppDispatch();
|
|
|
|
const details = useProBackendProDetails();
|
|
|
|
const mockSuccess = getFeatureFlagMemo('mockProRecoverButtonAlwaysSucceed');
|
|
const mockFail = getFeatureFlagMemo('mockProRecoverButtonAlwaysFail');
|
|
|
|
const mockRefetchSuccess = async () => {
|
|
const setProBackendIsLoading = (props: RequestActionArgs) =>
|
|
dispatch(proBackendDataActions.setIsLoading(props));
|
|
const setProBackendIsError = (props: RequestActionArgs) =>
|
|
dispatch(proBackendDataActions.setIsError(props));
|
|
|
|
if (details.isLoading) {
|
|
return;
|
|
}
|
|
setProBackendIsLoading({ key: 'details', result: true });
|
|
setProBackendIsError({ key: 'details', result: false });
|
|
await sleepFor(5000);
|
|
setProBackendIsLoading({ key: 'details', result: false });
|
|
};
|
|
|
|
const mockRefetchFail = async () => {
|
|
const setProBackendIsLoading = (props: RequestActionArgs) =>
|
|
dispatch(proBackendDataActions.setIsLoading(props));
|
|
const setProBackendIsError = (props: RequestActionArgs) =>
|
|
dispatch(proBackendDataActions.setIsError(props));
|
|
if (details.isLoading) {
|
|
return;
|
|
}
|
|
setProBackendIsLoading({ key: 'details', result: true });
|
|
setProBackendIsError({ key: 'details', result: false });
|
|
await sleepFor(5000);
|
|
setProBackendIsError({ key: 'details', result: true });
|
|
setProBackendIsLoading({ key: 'details', result: false });
|
|
};
|
|
|
|
const refetch = (args: WithCallerContext = {}) => {
|
|
if (details.isError || mockFail) {
|
|
void mockRefetchFail();
|
|
return;
|
|
}
|
|
|
|
if (mockSuccess) {
|
|
void mockRefetchSuccess();
|
|
return;
|
|
}
|
|
dispatch(proBackendDataActions.refreshGetProDetailsFromProBackend(args) as any);
|
|
};
|
|
|
|
return refetch;
|
|
}
|