Hide "become a sustainer" button if you're already a sustainer
This commit is contained in:
parent
7edf3763a8
commit
67b17ec317
22 changed files with 318 additions and 26 deletions
|
@ -441,9 +441,10 @@ message SyncMessage {
|
||||||
|
|
||||||
message FetchLatest {
|
message FetchLatest {
|
||||||
enum Type {
|
enum Type {
|
||||||
UNKNOWN = 0;
|
UNKNOWN = 0;
|
||||||
LOCAL_PROFILE = 1;
|
LOCAL_PROFILE = 1;
|
||||||
STORAGE_MANIFEST = 2;
|
STORAGE_MANIFEST = 2;
|
||||||
|
SUBSCRIPTION_STATUS = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
optional Type type = 1;
|
optional Type type = 1;
|
||||||
|
|
|
@ -137,4 +137,7 @@ message AccountRecord {
|
||||||
optional bool primarySendsSms = 18;
|
optional bool primarySendsSms = 18;
|
||||||
optional string e164 = 19;
|
optional string e164 = 19;
|
||||||
repeated string preferredReactionEmoji = 20;
|
repeated string preferredReactionEmoji = 20;
|
||||||
|
optional bytes subscriberId = 21;
|
||||||
|
optional string subscriberCurrencyCode = 22;
|
||||||
|
optional bool displayBadgesOnProfile = 23;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ import { initializeAllJobQueues } from './jobs/initializeAllJobQueues';
|
||||||
import { removeStorageKeyJobQueue } from './jobs/removeStorageKeyJobQueue';
|
import { removeStorageKeyJobQueue } from './jobs/removeStorageKeyJobQueue';
|
||||||
import { ourProfileKeyService } from './services/ourProfileKey';
|
import { ourProfileKeyService } from './services/ourProfileKey';
|
||||||
import { notificationService } from './services/notifications';
|
import { notificationService } from './services/notifications';
|
||||||
|
import { areWeASubscriberService } from './services/areWeASubscriber';
|
||||||
import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey';
|
import { shouldRespondWithProfileKey } from './util/shouldRespondWithProfileKey';
|
||||||
import { LatestQueue } from './util/LatestQueue';
|
import { LatestQueue } from './util/LatestQueue';
|
||||||
import { parseIntOrThrow } from './util/parseIntOrThrow';
|
import { parseIntOrThrow } from './util/parseIntOrThrow';
|
||||||
|
@ -346,6 +347,8 @@ export async function startApp(): Promise<void> {
|
||||||
onlineEventTarget: window,
|
onlineEventTarget: window,
|
||||||
storage: window.storage,
|
storage: window.storage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
areWeASubscriberService.update(window.storage, server);
|
||||||
});
|
});
|
||||||
|
|
||||||
const eventHandlerQueue = new window.PQueue({
|
const eventHandlerQueue = new window.PQueue({
|
||||||
|
@ -3477,6 +3480,11 @@ export async function startApp(): Promise<void> {
|
||||||
log.info('onFetchLatestSync: fetching latest manifest');
|
log.info('onFetchLatestSync: fetching latest manifest');
|
||||||
await window.Signal.Services.runStorageServiceSyncJob();
|
await window.Signal.Services.runStorageServiceSyncJob();
|
||||||
break;
|
break;
|
||||||
|
case FETCH_LATEST_ENUM.SUBSCRIPTION_STATUS:
|
||||||
|
log.info('onFetchLatestSync: fetching latest subscription status');
|
||||||
|
strictAssert(server, 'WebAPI not ready');
|
||||||
|
areWeASubscriberService.update(window.storage, server);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
log.info(`onFetchLatestSync: Unknown type encountered ${eventType}`);
|
log.info(`onFetchLatestSync: Unknown type encountered ${eventType}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ const i18n = setupI18n('en', enMessages);
|
||||||
const story = storiesOf('Components/BadgeDialog', module);
|
const story = storiesOf('Components/BadgeDialog', module);
|
||||||
|
|
||||||
const defaultProps: ComponentProps<typeof BadgeDialog> = {
|
const defaultProps: ComponentProps<typeof BadgeDialog> = {
|
||||||
|
areWeASubscriber: false,
|
||||||
badges: getFakeBadges(3),
|
badges: getFakeBadges(3),
|
||||||
firstName: 'Alice',
|
firstName: 'Alice',
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -95,3 +96,7 @@ story.add('Five badges', () => (
|
||||||
story.add('Many badges', () => (
|
story.add('Many badges', () => (
|
||||||
<BadgeDialog {...defaultProps} badges={getFakeBadges(50)} />
|
<BadgeDialog {...defaultProps} badges={getFakeBadges(50)} />
|
||||||
));
|
));
|
||||||
|
|
||||||
|
story.add('Many badges, user is a subscriber', () => (
|
||||||
|
<BadgeDialog {...defaultProps} areWeASubscriber badges={getFakeBadges(50)} />
|
||||||
|
));
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { BadgeCarouselIndex } from './BadgeCarouselIndex';
|
||||||
import { BadgeSustainerInstructionsDialog } from './BadgeSustainerInstructionsDialog';
|
import { BadgeSustainerInstructionsDialog } from './BadgeSustainerInstructionsDialog';
|
||||||
|
|
||||||
type PropsType = Readonly<{
|
type PropsType = Readonly<{
|
||||||
|
areWeASubscriber: boolean;
|
||||||
badges: ReadonlyArray<BadgeType>;
|
badges: ReadonlyArray<BadgeType>;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -53,6 +54,7 @@ export function BadgeDialog(props: PropsType): null | JSX.Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
function BadgeDialogWithBadges({
|
function BadgeDialogWithBadges({
|
||||||
|
areWeASubscriber,
|
||||||
badges,
|
badges,
|
||||||
firstName,
|
firstName,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -114,17 +116,19 @@ function BadgeDialogWithBadges({
|
||||||
title={title}
|
title={title}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
{!areWeASubscriber && (
|
||||||
className={classNames(
|
<Button
|
||||||
'BadgeDialog__instructions-button',
|
className={classNames(
|
||||||
currentBadge.category !== BadgeCategory.Donor &&
|
'BadgeDialog__instructions-button',
|
||||||
'BadgeDialog__instructions-button--hidden'
|
currentBadge.category !== BadgeCategory.Donor &&
|
||||||
)}
|
'BadgeDialog__instructions-button--hidden'
|
||||||
onClick={onShowInstructions}
|
)}
|
||||||
size={ButtonSize.Large}
|
onClick={onShowInstructions}
|
||||||
>
|
size={ButtonSize.Large}
|
||||||
{i18n('BadgeDialog__become-a-sustainer-button')}
|
>
|
||||||
</Button>
|
{i18n('BadgeDialog__become-a-sustainer-button')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<BadgeCarouselIndex
|
<BadgeCarouselIndex
|
||||||
currentIndex={currentBadgeIndex}
|
currentIndex={currentBadgeIndex}
|
||||||
totalCount={badges.length}
|
totalCount={badges.length}
|
||||||
|
|
|
@ -29,6 +29,7 @@ const defaultContact: ConversationType = getDefaultConversation({
|
||||||
});
|
});
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
|
areWeASubscriber: false,
|
||||||
areWeAdmin: boolean('areWeAdmin', overrideProps.areWeAdmin || false),
|
areWeAdmin: boolean('areWeAdmin', overrideProps.areWeAdmin || false),
|
||||||
badges: overrideProps.badges || [],
|
badges: overrideProps.badges || [],
|
||||||
contact: overrideProps.contact || defaultContact,
|
contact: overrideProps.contact || defaultContact,
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { SharedGroupNames } from '../SharedGroupNames';
|
||||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||||
|
|
||||||
export type PropsDataType = {
|
export type PropsDataType = {
|
||||||
|
areWeASubscriber: boolean;
|
||||||
areWeAdmin: boolean;
|
areWeAdmin: boolean;
|
||||||
badges: ReadonlyArray<BadgeType>;
|
badges: ReadonlyArray<BadgeType>;
|
||||||
contact?: ConversationType;
|
contact?: ConversationType;
|
||||||
|
@ -50,6 +51,7 @@ enum ContactModalView {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ContactModal = ({
|
export const ContactModal = ({
|
||||||
|
areWeASubscriber,
|
||||||
areWeAdmin,
|
areWeAdmin,
|
||||||
badges,
|
badges,
|
||||||
contact,
|
contact,
|
||||||
|
@ -219,6 +221,7 @@ export const ContactModal = ({
|
||||||
case ContactModalView.ShowingBadges:
|
case ContactModalView.ShowingBadges:
|
||||||
return (
|
return (
|
||||||
<BadgeDialog
|
<BadgeDialog
|
||||||
|
areWeASubscriber={areWeASubscriber}
|
||||||
badges={badges}
|
badges={badges}
|
||||||
firstName={contact.firstName}
|
firstName={contact.firstName}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
|
@ -37,6 +37,7 @@ const createProps = (hasGroupLink = false, expireTimer?: number): Props => ({
|
||||||
addMembers: async () => {
|
addMembers: async () => {
|
||||||
action('addMembers');
|
action('addMembers');
|
||||||
},
|
},
|
||||||
|
areWeASubscriber: false,
|
||||||
canEditGroupInfo: false,
|
canEditGroupInfo: false,
|
||||||
candidateContactsToAdd: times(10, () => getDefaultConversation()),
|
candidateContactsToAdd: times(10, () => getDefaultConversation()),
|
||||||
conversation: expireTimer
|
conversation: expireTimer
|
||||||
|
|
|
@ -55,6 +55,7 @@ enum ModalState {
|
||||||
|
|
||||||
export type StateProps = {
|
export type StateProps = {
|
||||||
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
addMembers: (conversationIds: ReadonlyArray<string>) => Promise<void>;
|
||||||
|
areWeASubscriber: boolean;
|
||||||
badges?: ReadonlyArray<BadgeType>;
|
badges?: ReadonlyArray<BadgeType>;
|
||||||
canEditGroupInfo: boolean;
|
canEditGroupInfo: boolean;
|
||||||
candidateContactsToAdd: Array<ConversationType>;
|
candidateContactsToAdd: Array<ConversationType>;
|
||||||
|
@ -109,6 +110,7 @@ export type Props = StateProps & ActionProps;
|
||||||
|
|
||||||
export const ConversationDetails: React.ComponentType<Props> = ({
|
export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
addMembers,
|
addMembers,
|
||||||
|
areWeASubscriber,
|
||||||
badges,
|
badges,
|
||||||
canEditGroupInfo,
|
canEditGroupInfo,
|
||||||
candidateContactsToAdd,
|
candidateContactsToAdd,
|
||||||
|
@ -316,6 +318,7 @@ export const ConversationDetails: React.ComponentType<Props> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ConversationDetailsHeader
|
<ConversationDetailsHeader
|
||||||
|
areWeASubscriber={areWeASubscriber}
|
||||||
badges={badges}
|
badges={badges}
|
||||||
canEdit={canEditGroupInfo}
|
canEdit={canEditGroupInfo}
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
|
|
|
@ -41,6 +41,7 @@ const Wrapper = (overrideProps: Partial<Props>) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConversationDetailsHeader
|
<ConversationDetailsHeader
|
||||||
|
areWeASubscriber={false}
|
||||||
conversation={createConversation()}
|
conversation={createConversation()}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
canEdit={false}
|
canEdit={false}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { BadgeDialog } from '../../BadgeDialog';
|
||||||
import type { BadgeType } from '../../../badges/types';
|
import type { BadgeType } from '../../../badges/types';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
areWeASubscriber: boolean;
|
||||||
badges?: ReadonlyArray<BadgeType>;
|
badges?: ReadonlyArray<BadgeType>;
|
||||||
canEdit: boolean;
|
canEdit: boolean;
|
||||||
conversation: ConversationType;
|
conversation: ConversationType;
|
||||||
|
@ -36,6 +37,7 @@ enum ConversationDetailsHeaderActiveModal {
|
||||||
const bem = bemGenerator('ConversationDetails-header');
|
const bem = bemGenerator('ConversationDetails-header');
|
||||||
|
|
||||||
export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
||||||
|
areWeASubscriber,
|
||||||
badges,
|
badges,
|
||||||
canEdit,
|
canEdit,
|
||||||
conversation,
|
conversation,
|
||||||
|
@ -128,6 +130,7 @@ export const ConversationDetailsHeader: React.ComponentType<Props> = ({
|
||||||
case ConversationDetailsHeaderActiveModal.ShowingBadges:
|
case ConversationDetailsHeaderActiveModal.ShowingBadges:
|
||||||
modal = (
|
modal = (
|
||||||
<BadgeDialog
|
<BadgeDialog
|
||||||
|
areWeASubscriber={areWeASubscriber}
|
||||||
badges={badges || []}
|
badges={badges || []}
|
||||||
firstName={conversation.firstName}
|
firstName={conversation.firstName}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
|
36
ts/services/areWeASubscriber.ts
Normal file
36
ts/services/areWeASubscriber.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import type { StorageInterface } from '../types/Storage.d';
|
||||||
|
import type { WebAPIType } from '../textsecure/WebAPI';
|
||||||
|
import { LatestQueue } from '../util/LatestQueue';
|
||||||
|
import { waitForOnline } from '../util/waitForOnline';
|
||||||
|
|
||||||
|
// This is only exported for testing.
|
||||||
|
export class AreWeASubscriberService {
|
||||||
|
private readonly queue = new LatestQueue();
|
||||||
|
|
||||||
|
update(
|
||||||
|
storage: Pick<StorageInterface, 'get' | 'put' | 'onready'>,
|
||||||
|
server: Pick<WebAPIType, 'getHasSubscription'>
|
||||||
|
): void {
|
||||||
|
this.queue.add(async () => {
|
||||||
|
await new Promise<void>(resolve => storage.onready(resolve));
|
||||||
|
|
||||||
|
const subscriberId = storage.get('subscriberId');
|
||||||
|
if (!subscriberId || !subscriberId.byteLength) {
|
||||||
|
storage.put('areWeASubscriber', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await waitForOnline(navigator, window);
|
||||||
|
|
||||||
|
storage.put(
|
||||||
|
'areWeASubscriber',
|
||||||
|
await server.getHasSubscription(subscriberId)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const areWeASubscriberService = new AreWeASubscriberService();
|
|
@ -293,6 +293,19 @@ export async function toAccountRecord(
|
||||||
);
|
);
|
||||||
|
|
||||||
accountRecord.pinnedConversations = pinnedConversations;
|
accountRecord.pinnedConversations = pinnedConversations;
|
||||||
|
|
||||||
|
const subscriberId = window.storage.get('subscriberId');
|
||||||
|
if (subscriberId instanceof Uint8Array) {
|
||||||
|
accountRecord.subscriberId = subscriberId;
|
||||||
|
}
|
||||||
|
const subscriberCurrencyCode = window.storage.get('subscriberCurrencyCode');
|
||||||
|
if (typeof subscriberCurrencyCode === 'string') {
|
||||||
|
accountRecord.subscriberCurrencyCode = subscriberCurrencyCode;
|
||||||
|
}
|
||||||
|
accountRecord.displayBadgesOnProfile = Boolean(
|
||||||
|
window.storage.get('displayBadgesOnProfile')
|
||||||
|
);
|
||||||
|
|
||||||
applyUnknownFields(accountRecord, conversation);
|
applyUnknownFields(accountRecord, conversation);
|
||||||
|
|
||||||
return accountRecord;
|
return accountRecord;
|
||||||
|
@ -845,6 +858,9 @@ export async function mergeAccountRecord(
|
||||||
universalExpireTimer,
|
universalExpireTimer,
|
||||||
e164: accountE164,
|
e164: accountE164,
|
||||||
preferredReactionEmoji: rawPreferredReactionEmoji,
|
preferredReactionEmoji: rawPreferredReactionEmoji,
|
||||||
|
subscriberId,
|
||||||
|
subscriberCurrencyCode,
|
||||||
|
displayBadgesOnProfile,
|
||||||
} = accountRecord;
|
} = accountRecord;
|
||||||
|
|
||||||
window.storage.put('read-receipt-setting', Boolean(readReceipts));
|
window.storage.put('read-receipt-setting', Boolean(readReceipts));
|
||||||
|
@ -1018,6 +1034,14 @@ export async function mergeAccountRecord(
|
||||||
window.storage.put('pinnedConversationIds', remotelyPinnedConversationIds);
|
window.storage.put('pinnedConversationIds', remotelyPinnedConversationIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subscriberId instanceof Uint8Array) {
|
||||||
|
window.storage.put('subscriberId', subscriberId);
|
||||||
|
}
|
||||||
|
if (typeof subscriberCurrencyCode === 'string') {
|
||||||
|
window.storage.put('subscriberCurrencyCode', subscriberCurrencyCode);
|
||||||
|
}
|
||||||
|
window.storage.put('displayBadgesOnProfile', Boolean(displayBadgesOnProfile));
|
||||||
|
|
||||||
const ourID = window.ConversationController.getOurConversationId();
|
const ourID = window.ConversationController.getOurConversationId();
|
||||||
|
|
||||||
if (!ourID) {
|
if (!ourID) {
|
||||||
|
|
|
@ -772,6 +772,7 @@ async function removeAllSignedPreKeys() {
|
||||||
const ITEM_KEYS: Partial<Record<ItemKeyType, Array<string>>> = {
|
const ITEM_KEYS: Partial<Record<ItemKeyType, Array<string>>> = {
|
||||||
senderCertificate: ['value.serialized'],
|
senderCertificate: ['value.serialized'],
|
||||||
senderCertificateNoE164: ['value.serialized'],
|
senderCertificateNoE164: ['value.serialized'],
|
||||||
|
subscriberId: ['value'],
|
||||||
profileKey: ['value'],
|
profileKey: ['value'],
|
||||||
};
|
};
|
||||||
async function createOrUpdateItem<K extends ItemKeyType>(data: ItemType<K>) {
|
async function createOrUpdateItem<K extends ItemKeyType>(data: ItemType<K>) {
|
||||||
|
|
|
@ -36,6 +36,8 @@ export type ItemsStateType = {
|
||||||
readonly preferredLeftPaneWidth?: number;
|
readonly preferredLeftPaneWidth?: number;
|
||||||
|
|
||||||
readonly preferredReactionEmoji?: Array<string>;
|
readonly preferredReactionEmoji?: Array<string>;
|
||||||
|
|
||||||
|
readonly areWeASubscriber?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
|
@ -20,6 +20,12 @@ const DEFAULT_PREFERRED_LEFT_PANE_WIDTH = 320;
|
||||||
|
|
||||||
export const getItems = (state: StateType): ItemsStateType => state.items;
|
export const getItems = (state: StateType): ItemsStateType => state.items;
|
||||||
|
|
||||||
|
export const getAreWeASubscriber = createSelector(
|
||||||
|
getItems,
|
||||||
|
({ areWeASubscriber }: Readonly<ItemsStateType>): boolean =>
|
||||||
|
Boolean(areWeASubscriber)
|
||||||
|
);
|
||||||
|
|
||||||
export const getUserAgent = createSelector(
|
export const getUserAgent = createSelector(
|
||||||
getItems,
|
getItems,
|
||||||
(state: ItemsStateType): string => state.userAgent as string
|
(state: ItemsStateType): string => state.userAgent as string
|
||||||
|
|
|
@ -7,6 +7,7 @@ import type { PropsDataType } from '../../components/conversation/ContactModal';
|
||||||
import { ContactModal } from '../../components/conversation/ContactModal';
|
import { ContactModal } from '../../components/conversation/ContactModal';
|
||||||
import type { StateType } from '../reducer';
|
import type { StateType } from '../reducer';
|
||||||
|
|
||||||
|
import { getAreWeASubscriber } from '../selectors/items';
|
||||||
import { getIntl, getTheme } from '../selectors/user';
|
import { getIntl, getTheme } from '../selectors/user';
|
||||||
import { getBadgesSelector } from '../selectors/badges';
|
import { getBadgesSelector } from '../selectors/badges';
|
||||||
import { getConversationSelector } from '../selectors/conversations';
|
import { getConversationSelector } from '../selectors/conversations';
|
||||||
|
@ -35,6 +36,7 @@ const mapStateToProps = (state: StateType): PropsDataType => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
areWeASubscriber: getAreWeASubscriber(state),
|
||||||
areWeAdmin,
|
areWeAdmin,
|
||||||
badges: getBadgesSelector(state)(contact.badges),
|
badges: getBadgesSelector(state)(contact.badges),
|
||||||
contact,
|
contact,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
getConversationByUuidSelector,
|
getConversationByUuidSelector,
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
||||||
|
import { getAreWeASubscriber } from '../selectors/items';
|
||||||
import { getIntl, getTheme } from '../selectors/user';
|
import { getIntl, getTheme } from '../selectors/user';
|
||||||
import type { MediaItemType } from '../../types/MediaItem';
|
import type { MediaItemType } from '../../types/MediaItem';
|
||||||
import {
|
import {
|
||||||
|
@ -82,6 +83,7 @@ const mapStateToProps = (
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...props,
|
...props,
|
||||||
|
areWeASubscriber: getAreWeASubscriber(state),
|
||||||
badges,
|
badges,
|
||||||
canEditGroupInfo,
|
canEditGroupInfo,
|
||||||
candidateContactsToAdd,
|
candidateContactsToAdd,
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import {
|
import {
|
||||||
|
getAreWeASubscriber,
|
||||||
getEmojiSkinTone,
|
getEmojiSkinTone,
|
||||||
getPinnedConversationIds,
|
getPinnedConversationIds,
|
||||||
getPreferredLeftPaneWidth,
|
getPreferredLeftPaneWidth,
|
||||||
|
@ -21,6 +22,21 @@ describe('both/state/selectors/items', () => {
|
||||||
} as any;
|
} as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('#getAreWeASubscriber', () => {
|
||||||
|
it('returns false if the value is not in storage', () => {
|
||||||
|
assert.isFalse(getAreWeASubscriber(getRootState({})));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the value in storage', () => {
|
||||||
|
assert.isFalse(
|
||||||
|
getAreWeASubscriber(getRootState({ areWeASubscriber: false }))
|
||||||
|
);
|
||||||
|
assert.isTrue(
|
||||||
|
getAreWeASubscriber(getRootState({ areWeASubscriber: true }))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#getEmojiSkinTone', () => {
|
describe('#getEmojiSkinTone', () => {
|
||||||
it('returns 0 if passed anything invalid', () => {
|
it('returns 0 if passed anything invalid', () => {
|
||||||
[
|
[
|
||||||
|
|
130
ts/test-electron/services/areWeASubscriber_test.ts
Normal file
130
ts/test-electron/services/areWeASubscriber_test.ts
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
// Copyright 2021 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
import { AreWeASubscriberService } from '../../services/areWeASubscriber';
|
||||||
|
import { explodePromise } from '../../util/explodePromise';
|
||||||
|
|
||||||
|
describe('"are we a subscriber?" service', () => {
|
||||||
|
const subscriberId = new Uint8Array([1, 2, 3]);
|
||||||
|
const fakeStorageDefaults = {
|
||||||
|
onready: sinon.stub().callsArg(0),
|
||||||
|
get: sinon.stub().withArgs('subscriberId').returns(subscriberId),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sandbox: sinon.SinonSandbox;
|
||||||
|
let service: AreWeASubscriberService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.createSandbox();
|
||||||
|
|
||||||
|
service = new AreWeASubscriberService();
|
||||||
|
sandbox.stub(navigator, 'onLine').get(() => true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores false if there's no local subscriber ID", done => {
|
||||||
|
const fakeServer = { getHasSubscription: sandbox.stub() };
|
||||||
|
const fakeStorage = {
|
||||||
|
...fakeStorageDefaults,
|
||||||
|
get: () => undefined,
|
||||||
|
put: sandbox.stub().callsFake((key, value) => {
|
||||||
|
assert.strictEqual(key, 'areWeASubscriber');
|
||||||
|
assert.isFalse(value);
|
||||||
|
done();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
service.update(fakeStorage, fakeServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't make a network request if there's no local subscriber ID", done => {
|
||||||
|
const fakeServer = { getHasSubscription: sandbox.stub() };
|
||||||
|
const fakeStorage = {
|
||||||
|
...fakeStorageDefaults,
|
||||||
|
get: () => undefined,
|
||||||
|
put: sandbox.stub().callsFake(() => {
|
||||||
|
sinon.assert.notCalled(fakeServer.getHasSubscription);
|
||||||
|
done();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
service.update(fakeStorage, fakeServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requests the subscriber ID from the server', done => {
|
||||||
|
const fakeServer = { getHasSubscription: sandbox.stub().resolves(false) };
|
||||||
|
const fakeStorage = {
|
||||||
|
...fakeStorageDefaults,
|
||||||
|
put: sandbox
|
||||||
|
.stub()
|
||||||
|
.withArgs('areWeASubscriber')
|
||||||
|
.callsFake(() => {
|
||||||
|
sinon.assert.calledWithExactly(
|
||||||
|
fakeServer.getHasSubscription,
|
||||||
|
subscriberId
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
service.update(fakeStorage, fakeServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores when we're not a subscriber", done => {
|
||||||
|
const fakeServer = { getHasSubscription: sandbox.stub().resolves(false) };
|
||||||
|
const fakeStorage = {
|
||||||
|
...fakeStorageDefaults,
|
||||||
|
put: sandbox.stub().callsFake((key, value) => {
|
||||||
|
assert.strictEqual(key, 'areWeASubscriber');
|
||||||
|
assert.isFalse(value);
|
||||||
|
done();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
service.update(fakeStorage, fakeServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores when we're a subscriber", done => {
|
||||||
|
const fakeServer = { getHasSubscription: sandbox.stub().resolves(true) };
|
||||||
|
const fakeStorage = {
|
||||||
|
...fakeStorageDefaults,
|
||||||
|
put: sandbox.stub().callsFake((key, value) => {
|
||||||
|
assert.strictEqual(key, 'areWeASubscriber');
|
||||||
|
assert.isTrue(value);
|
||||||
|
done();
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
service.update(fakeStorage, fakeServer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only runs one request at a time and enqueues one other', async () => {
|
||||||
|
const allDone = explodePromise<void>();
|
||||||
|
let putCallCount = 0;
|
||||||
|
|
||||||
|
const fakeServer = { getHasSubscription: sandbox.stub().resolves(false) };
|
||||||
|
const fakeStorage = {
|
||||||
|
...fakeStorageDefaults,
|
||||||
|
put: sandbox.stub().callsFake(() => {
|
||||||
|
putCallCount += 1;
|
||||||
|
if (putCallCount === 2) {
|
||||||
|
allDone.resolve();
|
||||||
|
} else if (putCallCount > 2) {
|
||||||
|
throw new Error('Unexpected call to storage put');
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
service.update(fakeStorage, fakeServer);
|
||||||
|
service.update(fakeStorage, fakeServer);
|
||||||
|
service.update(fakeStorage, fakeServer);
|
||||||
|
service.update(fakeStorage, fakeServer);
|
||||||
|
service.update(fakeStorage, fakeServer);
|
||||||
|
|
||||||
|
await allDone.promise;
|
||||||
|
|
||||||
|
sinon.assert.calledTwice(fakeServer.getHasSubscription);
|
||||||
|
sinon.assert.calledTwice(fakeStorage.put);
|
||||||
|
});
|
||||||
|
});
|
|
@ -26,6 +26,7 @@ import Long from 'long';
|
||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
|
|
||||||
import { assert, strictAssert } from '../util/assert';
|
import { assert, strictAssert } from '../util/assert';
|
||||||
|
import { isRecord } from '../util/isRecord';
|
||||||
import * as durations from '../util/durations';
|
import * as durations from '../util/durations';
|
||||||
import { getUserAgent } from '../util/getUserAgent';
|
import { getUserAgent } from '../util/getUserAgent';
|
||||||
import { getStreamWithTimeout } from '../util/getStreamWithTimeout';
|
import { getStreamWithTimeout } from '../util/getStreamWithTimeout';
|
||||||
|
@ -169,7 +170,6 @@ type RedactUrl = (url: string) => string;
|
||||||
|
|
||||||
type PromiseAjaxOptionsType = {
|
type PromiseAjaxOptionsType = {
|
||||||
socketManager?: SocketManager;
|
socketManager?: SocketManager;
|
||||||
accessKey?: string;
|
|
||||||
basicAuth?: string;
|
basicAuth?: string;
|
||||||
certificateAuthority?: string;
|
certificateAuthority?: string;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
|
@ -191,12 +191,20 @@ type PromiseAjaxOptionsType = {
|
||||||
stack?: string;
|
stack?: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
type: HTTPCodeType;
|
type: HTTPCodeType;
|
||||||
unauthenticated?: boolean;
|
|
||||||
user?: string;
|
user?: string;
|
||||||
validateResponse?: any;
|
validateResponse?: any;
|
||||||
version: string;
|
version: string;
|
||||||
abortSignal?: AbortSignal;
|
abortSignal?: AbortSignal;
|
||||||
};
|
} & (
|
||||||
|
| {
|
||||||
|
unauthenticated?: false;
|
||||||
|
accessKey?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
unauthenticated: true;
|
||||||
|
accessKey: undefined | string;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
type JSONWithDetailsType = {
|
type JSONWithDetailsType = {
|
||||||
data: unknown;
|
data: unknown;
|
||||||
|
@ -321,13 +329,10 @@ async function _promiseAjax(
|
||||||
if (basicAuth) {
|
if (basicAuth) {
|
||||||
fetchOptions.headers.Authorization = `Basic ${basicAuth}`;
|
fetchOptions.headers.Authorization = `Basic ${basicAuth}`;
|
||||||
} else if (unauthenticated) {
|
} else if (unauthenticated) {
|
||||||
if (!accessKey) {
|
if (accessKey) {
|
||||||
throw new Error(
|
// Access key is already a Base64 string
|
||||||
'_promiseAjax: mode is unauthenticated, but accessKey was not provided'
|
fetchOptions.headers['Unidentified-Access-Key'] = accessKey;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// Access key is already a Base64 string
|
|
||||||
fetchOptions.headers['Unidentified-Access-Key'] = accessKey;
|
|
||||||
} else if (options.user && options.password) {
|
} else if (options.user && options.password) {
|
||||||
const auth = Bytes.toBase64(
|
const auth = Bytes.toBase64(
|
||||||
Bytes.fromString(`${options.user}:${options.password}`)
|
Bytes.fromString(`${options.user}:${options.password}`)
|
||||||
|
@ -542,6 +547,7 @@ const URL_CALLS = {
|
||||||
storageModify: 'v1/storage/',
|
storageModify: 'v1/storage/',
|
||||||
storageRead: 'v1/storage/read',
|
storageRead: 'v1/storage/read',
|
||||||
storageToken: 'v1/storage/auth',
|
storageToken: 'v1/storage/auth',
|
||||||
|
subscriptions: 'v1/subscription',
|
||||||
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
|
||||||
updateDeviceName: 'v1/accounts/name',
|
updateDeviceName: 'v1/accounts/name',
|
||||||
username: 'v1/accounts/username',
|
username: 'v1/accounts/username',
|
||||||
|
@ -608,7 +614,6 @@ export type MessageType = Readonly<{
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type AjaxOptionsType = {
|
type AjaxOptionsType = {
|
||||||
accessKey?: string;
|
|
||||||
basicAuth?: string;
|
basicAuth?: string;
|
||||||
call: keyof typeof URL_CALLS;
|
call: keyof typeof URL_CALLS;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
|
@ -622,11 +627,19 @@ type AjaxOptionsType = {
|
||||||
responseType?: 'json' | 'bytes' | 'byteswithdetails' | 'stream';
|
responseType?: 'json' | 'bytes' | 'byteswithdetails' | 'stream';
|
||||||
schema?: unknown;
|
schema?: unknown;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
unauthenticated?: boolean;
|
|
||||||
urlParameters?: string;
|
urlParameters?: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
validateResponse?: any;
|
validateResponse?: any;
|
||||||
};
|
} & (
|
||||||
|
| {
|
||||||
|
unauthenticated?: false;
|
||||||
|
accessKey?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
unauthenticated: true;
|
||||||
|
accessKey: undefined | string;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export type WebAPIConnectOptionsType = WebAPICredentials & {
|
export type WebAPIConnectOptionsType = WebAPICredentials & {
|
||||||
useWebSocket?: boolean;
|
useWebSocket?: boolean;
|
||||||
|
@ -753,6 +766,7 @@ export type WebAPIType = {
|
||||||
getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<Uint8Array>;
|
getAttachment: (cdnKey: string, cdnNumber?: number) => Promise<Uint8Array>;
|
||||||
getAvatar: (path: string) => Promise<Uint8Array>;
|
getAvatar: (path: string) => Promise<Uint8Array>;
|
||||||
getDevices: () => Promise<GetDevicesResultType>;
|
getDevices: () => Promise<GetDevicesResultType>;
|
||||||
|
getHasSubscription: (subscriberId: Uint8Array) => Promise<boolean>;
|
||||||
getGroup: (options: GroupCredentialsType) => Promise<Proto.Group>;
|
getGroup: (options: GroupCredentialsType) => Promise<Proto.Group>;
|
||||||
getGroupFromLink: (
|
getGroupFromLink: (
|
||||||
inviteLinkPassword: string,
|
inviteLinkPassword: string,
|
||||||
|
@ -1092,6 +1106,7 @@ export function initialize({
|
||||||
getGroupExternalCredential,
|
getGroupExternalCredential,
|
||||||
getGroupFromLink,
|
getGroupFromLink,
|
||||||
getGroupLog,
|
getGroupLog,
|
||||||
|
getHasSubscription,
|
||||||
getIceServers,
|
getIceServers,
|
||||||
getKeysForIdentifier,
|
getKeysForIdentifier,
|
||||||
getKeysForIdentifierUnauth,
|
getKeysForIdentifierUnauth,
|
||||||
|
@ -2493,6 +2508,27 @@ export function initialize({
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getHasSubscription(
|
||||||
|
subscriberId: Uint8Array
|
||||||
|
): Promise<boolean> {
|
||||||
|
const formattedId = toWebSafeBase64(Bytes.toBase64(subscriberId));
|
||||||
|
const data = await _ajax({
|
||||||
|
call: 'subscriptions',
|
||||||
|
httpType: 'GET',
|
||||||
|
urlParameters: `/${formattedId}`,
|
||||||
|
responseType: 'json',
|
||||||
|
unauthenticated: true,
|
||||||
|
accessKey: undefined,
|
||||||
|
redactUrl: _createRedactor(formattedId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
isRecord(data) &&
|
||||||
|
isRecord(data.subscription) &&
|
||||||
|
Boolean(data.subscription.active)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function getProvisioningResource(
|
function getProvisioningResource(
|
||||||
handler: IRequestHandler
|
handler: IRequestHandler
|
||||||
): Promise<WebSocketResource> {
|
): Promise<WebSocketResource> {
|
||||||
|
|
4
ts/types/Storage.d.ts
vendored
4
ts/types/Storage.d.ts
vendored
|
@ -134,6 +134,10 @@ export type StorageAccessType = {
|
||||||
paymentAddress: string;
|
paymentAddress: string;
|
||||||
zoomFactor: ZoomFactorType;
|
zoomFactor: ZoomFactorType;
|
||||||
preferredLeftPaneWidth: number;
|
preferredLeftPaneWidth: number;
|
||||||
|
areWeASubscriber: boolean;
|
||||||
|
subscriberId: Uint8Array;
|
||||||
|
subscriberCurrencyCode: string;
|
||||||
|
displayBadgesOnProfile: boolean;
|
||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
senderCertificateWithUuid: never;
|
senderCertificateWithUuid: never;
|
||||||
|
|
Loading…
Reference in a new issue