mirror of
https://github.com/tappollo/booster.git
synced 2026-04-29 01:55:54 +08:00
merge/hanzo (#35)
This commit is contained in:
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.rndiffapp;
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
import com.facebook.flipper.android.utils.FlipperUtils;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.react.ReactFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import okhttp3.OkHttpClient;
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (FlipperUtils.shouldEnableFlipper(context)) {
|
||||
final FlipperClient client = AndroidFlipperClient.getInstance(context);
|
||||
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
|
||||
client.addPlugin(new ReactFlipperPlugin());
|
||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
|
||||
NetworkingModule.setCustomClientBuilder(
|
||||
new NetworkingModule.CustomClientBuilder() {
|
||||
@Override
|
||||
public void apply(OkHttpClient.Builder builder) {
|
||||
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
|
||||
}
|
||||
});
|
||||
client.addPlugin(networkFlipperPlugin);
|
||||
client.start();
|
||||
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
|
||||
// Hence we run if after all native modules have been initialized
|
||||
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext == null) {
|
||||
reactInstanceManager.addReactInstanceEventListener(
|
||||
new ReactInstanceManager.ReactInstanceEventListener() {
|
||||
@Override
|
||||
public void onReactContextInitialized(ReactContext reactContext) {
|
||||
reactInstanceManager.removeReactInstanceEventListener(this);
|
||||
reactContext.runOnNativeModulesQueueThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||
import { StatusBar } from "react-native";
|
||||
import ErrorBoundary from "react-native-error-boundary";
|
||||
import crashlytics from "@react-native-firebase/crashlytics";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
|
||||
const App = () => (
|
||||
<ErrorBoundary
|
||||
@@ -18,7 +19,9 @@ const App = () => (
|
||||
<StatusBar barStyle="dark-content" backgroundColor="white" />
|
||||
<PaperProvider theme={theme}>
|
||||
<ActionSheetProvider>
|
||||
<Routes />
|
||||
<SafeAreaProvider>
|
||||
<Routes />
|
||||
</SafeAreaProvider>
|
||||
</ActionSheetProvider>
|
||||
</PaperProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
73
app/src/booster/components/useModalInput.tsx
Normal file
73
app/src/booster/components/useModalInput.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Alert, TextInputProps } from "react-native";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Button, Dialog, Portal, TextInput } from "react-native-paper";
|
||||
|
||||
const useModalInput = ({
|
||||
title,
|
||||
...props
|
||||
}: { title: string } & TextInputProps) => {
|
||||
const [onResult, setOnResult] = useState<
|
||||
(result: string) => void | Promise<void>
|
||||
>();
|
||||
const [text, setText] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const element = (
|
||||
<Portal>
|
||||
<Dialog
|
||||
css={`
|
||||
margin-top: -200px;
|
||||
`}
|
||||
visible={onResult != null}
|
||||
onDismiss={() => setOnResult(undefined)}
|
||||
>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<TextInput
|
||||
autoFocus={true}
|
||||
dense={true}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
{...props}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
<Dialog.Actions>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={!text}
|
||||
onPress={async () => {
|
||||
const result = onResult?.(text);
|
||||
if (result == null) {
|
||||
setOnResult(undefined);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
await result;
|
||||
setOnResult(undefined);
|
||||
} catch (e) {
|
||||
Alert.alert(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
const getInput = useCallback(
|
||||
(action: typeof onResult) => {
|
||||
setText("");
|
||||
setOnResult(() => action);
|
||||
},
|
||||
[setOnResult, setText, onResult]
|
||||
);
|
||||
return {
|
||||
getInput,
|
||||
element,
|
||||
};
|
||||
};
|
||||
|
||||
export default useModalInput;
|
||||
@@ -12,6 +12,7 @@ export interface ActionSheetOptions {
|
||||
message?: string;
|
||||
tintColor?: string;
|
||||
anchor?: number;
|
||||
defaultCancel?: boolean;
|
||||
}
|
||||
|
||||
export const useActionSheet = () => {
|
||||
@@ -21,14 +22,18 @@ export const useActionSheet = () => {
|
||||
showActionSheetWithOptions(
|
||||
{
|
||||
...options,
|
||||
options: items.map((i) => i.title),
|
||||
cancelButtonIndex: items.findIndex((i) => i.type === "cancel"),
|
||||
options: items
|
||||
.map((i) => i.title)
|
||||
.concat(options.defaultCancel ? ["Cancel"] : []),
|
||||
cancelButtonIndex: options.defaultCancel
|
||||
? items.length
|
||||
: items.findIndex((i) => i.type === "cancel"),
|
||||
destructiveButtonIndex: items.findIndex(
|
||||
(i) => i.type === "destructive"
|
||||
),
|
||||
},
|
||||
(i) => {
|
||||
items[i].onPress?.();
|
||||
items[i]?.onPress?.();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import firestore, {
|
||||
FirebaseFirestoreTypes,
|
||||
} from "@react-native-firebase/firestore";
|
||||
import { CollectionReference, DocumentReference } from "./firestoreHooks";
|
||||
import {
|
||||
CollectionReference,
|
||||
DocumentReference,
|
||||
LoadingErrorState,
|
||||
useListenDocument,
|
||||
} from "./firestoreHooks";
|
||||
|
||||
type GetOptions = FirebaseFirestoreTypes.GetOptions;
|
||||
|
||||
@@ -10,6 +15,7 @@ export const collection = (collectionId: string): CollectionReference => {
|
||||
};
|
||||
|
||||
type DocTypedWrapper<T> = {
|
||||
useListen: () => LoadingErrorState<T>;
|
||||
read: (options?: GetOptions) => Promise<T>;
|
||||
update: (value: Partial<T>) => Promise<void>;
|
||||
listen: (callback: (value: T) => void) => () => void;
|
||||
@@ -17,26 +23,35 @@ type DocTypedWrapper<T> = {
|
||||
};
|
||||
|
||||
export function makeDocAsType<T>(
|
||||
doc: () => DocumentReference
|
||||
docGen: () => DocumentReference
|
||||
): DocTypedWrapper<T> {
|
||||
let doc: DocumentReference;
|
||||
const lazyDoc = () => {
|
||||
doc = doc ?? docGen();
|
||||
return doc;
|
||||
};
|
||||
async function read(options?: GetOptions) {
|
||||
const snapshot = await doc().get(options);
|
||||
const snapshot = await lazyDoc().get(options);
|
||||
const value: T = snapshot.data() as any;
|
||||
if (!snapshot.exists || value == null) {
|
||||
throw new Error(`Doc ${doc().path} does not exist`);
|
||||
throw new Error(`Doc ${lazyDoc().path} does not exist`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
async function update(newValue: Partial<T>) {
|
||||
return await doc().set(newValue, { merge: true });
|
||||
return await lazyDoc().set(newValue, { merge: true });
|
||||
}
|
||||
function listen(callback: (value: T) => void) {
|
||||
return doc().onSnapshot((snapshot) => {
|
||||
return lazyDoc().onSnapshot((snapshot) => {
|
||||
callback(snapshot.data() as any);
|
||||
});
|
||||
}
|
||||
function useListen() {
|
||||
return useListenDocument<T>(lazyDoc());
|
||||
}
|
||||
return {
|
||||
ref: doc,
|
||||
ref: lazyDoc,
|
||||
useListen,
|
||||
read,
|
||||
update,
|
||||
listen,
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface Profile {
|
||||
name: string;
|
||||
avatar: string;
|
||||
email?: string;
|
||||
onboardingCompleted?: number;
|
||||
}
|
||||
|
||||
// Private profile only you can see or update
|
||||
@@ -28,12 +29,14 @@ export interface PrivateProfile {
|
||||
pushTokens: {
|
||||
[deviceId: string]: string;
|
||||
};
|
||||
defaultPayment?: PaymentMethod;
|
||||
}
|
||||
|
||||
// System record only you can see, but not mutate
|
||||
export interface ReadonlyProfile {
|
||||
accountBalance: number;
|
||||
behaviorScore: number;
|
||||
stripeCustomerId?: string;
|
||||
}
|
||||
|
||||
// Chat Related
|
||||
@@ -63,3 +66,15 @@ export interface UserStatus {
|
||||
conversationId?: string;
|
||||
isTyping?: boolean;
|
||||
}
|
||||
|
||||
export interface AddPaymentMethodInput {
|
||||
stripeToken: string;
|
||||
}
|
||||
|
||||
export interface PaymentMethod {
|
||||
id: string;
|
||||
last4: string;
|
||||
brand: string;
|
||||
exp_year: number;
|
||||
exp_month: number;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import auth from "@react-native-firebase/auth";
|
||||
import { FirebaseFirestoreTypes } from "@react-native-firebase/firestore";
|
||||
import firestore, {
|
||||
FirebaseFirestoreTypes,
|
||||
} from "@react-native-firebase/firestore";
|
||||
import { PrivateProfile, Profile, ReadonlyProfile } from "./types";
|
||||
import { collection, makeDocAsType } from "./firebase/firestore";
|
||||
import functions from "@react-native-firebase/functions";
|
||||
import { useEffect, useState } from "react";
|
||||
import { keyOf } from "./firebase/firestoreHooks";
|
||||
import DeviceInfo from "react-native-device-info";
|
||||
|
||||
type DocumentSnapshot = FirebaseFirestoreTypes.DocumentSnapshot;
|
||||
|
||||
@@ -31,7 +37,7 @@ export const typedReadonlyProfile = makeDocAsType<ReadonlyProfile>(() =>
|
||||
|
||||
export const userFinishedSignUp = async () => {
|
||||
const valid = (snapshot: Profile) => {
|
||||
return snapshot.name && snapshot.avatar;
|
||||
return snapshot.onboardingCompleted! > 0;
|
||||
};
|
||||
const cached = await typedProfile.read({ source: "cache" }).catch(() => null);
|
||||
if (cached != null && valid(cached)) {
|
||||
@@ -42,3 +48,31 @@ export const userFinishedSignUp = async () => {
|
||||
.catch(() => null);
|
||||
return server != null && valid(server);
|
||||
};
|
||||
|
||||
export const promoteToAdmin = async (password: string) => {
|
||||
await functions().httpsCallable("user-promoteToAdmin")({ password });
|
||||
await auth().currentUser?.getIdToken(true);
|
||||
};
|
||||
|
||||
export const useIsAdmin = () => {
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
useEffect(() => {
|
||||
auth()
|
||||
.currentUser!.getIdTokenResult()
|
||||
.then((value) => setIsAdmin(value.claims.isAdmin));
|
||||
}, []);
|
||||
return isAdmin;
|
||||
};
|
||||
|
||||
export const logout = async () => {
|
||||
try {
|
||||
await typedPrivateProfile
|
||||
.ref()
|
||||
.update(
|
||||
keyOf<PrivateProfile>("pushTokens") + "." + DeviceInfo.getUniqueId(),
|
||||
firestore.FieldValue.delete()
|
||||
);
|
||||
} finally {
|
||||
await auth().signOut();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,3 +34,7 @@ export const useKeyboardManagerOnFocus = (enable: boolean) => {
|
||||
export function compose<A, B, C>(l: (a: A) => B, r: (b: B) => C): (a: A) => C {
|
||||
return (a) => r(l(a));
|
||||
}
|
||||
|
||||
export function assertNever(x: never): never {
|
||||
throw new Error("Unexpected object: " + x);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Home from "./home";
|
||||
import { trackScreenNavigation } from "../functions/analytics";
|
||||
import auth from "@react-native-firebase/auth";
|
||||
import { userFinishedSignUp } from "../functions/user";
|
||||
import { navigationTheme } from "../styles/theme";
|
||||
|
||||
export const AppRouteContext = createContext<{
|
||||
resetRoute?: () => void;
|
||||
@@ -45,6 +46,7 @@ const Routes = () => {
|
||||
<NavigationContainer
|
||||
key={isLoggedIn}
|
||||
onStateChange={trackScreenNavigation}
|
||||
theme={navigationTheme}
|
||||
>
|
||||
{isLoggedIn === "home" ? <Home /> : <OnBoarding />}
|
||||
</NavigationContainer>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useAppLaunchAfterLogin } from "../../functions/app";
|
||||
import ChatDetailPage, { ChatDetailPageParams } from "../chat/ChatDetailPage";
|
||||
import ChatListPage from "../chat/ChatListPage";
|
||||
import ChatContactListPage from "../chat/ChatContactListPage";
|
||||
import OnboardingProfile from "../onboarding/OnboardingProfile";
|
||||
|
||||
type BottomTabParams = {
|
||||
homePage: undefined;
|
||||
@@ -61,6 +62,9 @@ export type HomeNavStackParams = {
|
||||
homeTab: undefined;
|
||||
chatDetail: ChatDetailPageParams;
|
||||
chatContactList: undefined;
|
||||
editProfile: {
|
||||
edit: true;
|
||||
};
|
||||
};
|
||||
|
||||
const HomeNavStack = createStackNavigator<HomeNavStackParams>();
|
||||
@@ -95,6 +99,7 @@ const HomeNav = () => {
|
||||
component={ChatContactListPage}
|
||||
options={{ title: "Contacts" }}
|
||||
/>
|
||||
<HomeNavStack.Screen name="editProfile" component={OnboardingProfile} />
|
||||
</HomeNavStack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,12 +10,16 @@ import { useListenDocument } from "../../functions/firebase/firestoreHooks";
|
||||
import { currentUser, typedProfile } from "../../functions/user";
|
||||
import { Profile } from "../../functions/types";
|
||||
import { ActivityIndicator, TextInput } from "react-native-paper";
|
||||
import { StackNavigationProp } from "@react-navigation/stack";
|
||||
import { OnboardingStackParams } from "./index";
|
||||
import { RouteProp, useRoute } from "@react-navigation/core";
|
||||
import { HomeNavStackParams } from "../home";
|
||||
import { AppRouteContext } from "../Routes";
|
||||
|
||||
const AvatarButton = styled.TouchableOpacity`
|
||||
margin-top: 10px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 60px;
|
||||
align-self: center;
|
||||
background-color: #cccccc;
|
||||
@@ -34,9 +38,15 @@ const Avatar = styled(FastImage)`
|
||||
border-radius: 60px;
|
||||
`;
|
||||
|
||||
const OnboardingProfile = () => {
|
||||
const { resetRoute } = useContext(AppRouteContext);
|
||||
const [nameInput, setNameInput] = useState("");
|
||||
const OnboardingProfile = ({
|
||||
navigation,
|
||||
}: {
|
||||
navigation: StackNavigationProp<OnboardingStackParams>;
|
||||
}) => {
|
||||
const route = useRoute<RouteProp<HomeNavStackParams, "editProfile">>();
|
||||
const isEditing = route.params?.edit;
|
||||
const [nameInput, setNameInput] = useState<string>();
|
||||
const [emailInput, setEmailInput] = useState<string>();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { value, update } = useListenDocument<Profile>(typedProfile.ref());
|
||||
const {
|
||||
@@ -46,35 +56,60 @@ const OnboardingProfile = () => {
|
||||
serverImage,
|
||||
} = usePickAndUploadImage();
|
||||
const avatar = serverImage || value?.avatar || currentUser().photoURL;
|
||||
const name = nameInput || value?.name || currentUser().displayName;
|
||||
const name = nameInput ?? value?.name ?? currentUser().displayName ?? "";
|
||||
const email = emailInput ?? value?.email ?? currentUser().email ?? "";
|
||||
const { resetRoute } = useContext(AppRouteContext);
|
||||
return (
|
||||
<PageContainer>
|
||||
<BigTitle>Choose your{"\n"}name and avatar</BigTitle>
|
||||
{!isEditing && <BigTitle>Choose your{"\n"}name and avatar</BigTitle>}
|
||||
<AvatarButton onPress={pick}>
|
||||
{avatar && (
|
||||
<Avatar
|
||||
source={{
|
||||
uri: localImage || avatar,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Avatar
|
||||
source={{
|
||||
uri: localImage || avatar || "",
|
||||
}}
|
||||
/>
|
||||
{isUploading && <ActivityIndicator />}
|
||||
</AvatarButton>
|
||||
<TextInput
|
||||
autoCapitalize="words"
|
||||
mode="outlined"
|
||||
label="Name"
|
||||
value={name || ""}
|
||||
value={name}
|
||||
onChangeText={setNameInput}
|
||||
/>
|
||||
<TextInput
|
||||
css={`
|
||||
margin: 10px 0;
|
||||
`}
|
||||
autoCapitalize="none"
|
||||
keyboardType="email-address"
|
||||
mode="outlined"
|
||||
label="Email"
|
||||
value={email}
|
||||
onChangeText={setEmailInput}
|
||||
/>
|
||||
<BigButton
|
||||
loading={saving}
|
||||
disabled={isUploading || avatar == null || !name}
|
||||
disabled={isUploading || !avatar || !name || !email}
|
||||
onPress={async () => {
|
||||
try {
|
||||
if (!validateEmail(email)) {
|
||||
Alert.alert("Please input a valid email address");
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
await update({ avatar: avatar!, name: name! });
|
||||
await update({
|
||||
avatar: avatar!,
|
||||
name,
|
||||
email,
|
||||
onboardingCompleted: 1,
|
||||
});
|
||||
setSaving(false);
|
||||
resetRoute?.();
|
||||
if (!isEditing) {
|
||||
resetRoute?.();
|
||||
} else {
|
||||
navigation.goBack();
|
||||
}
|
||||
} catch (e) {
|
||||
Alert.alert(e.message);
|
||||
setSaving(false);
|
||||
@@ -87,4 +122,10 @@ const OnboardingProfile = () => {
|
||||
);
|
||||
};
|
||||
|
||||
function validateEmail(email: string) {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(String(email).toLowerCase());
|
||||
}
|
||||
|
||||
export default OnboardingProfile;
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import styled from "styled-components/native";
|
||||
import React from "react";
|
||||
import React, { useContext } from "react";
|
||||
import LogoutButton from "./components/LogoutButton";
|
||||
import { code, hash } from "../../../version.json";
|
||||
import { Text } from "react-native";
|
||||
import { useActionSheet } from "../../functions/actionsheet";
|
||||
import { logout, promoteToAdmin, typedProfile } from "../../functions/user";
|
||||
import useModalInput from "../../components/useModalInput";
|
||||
import { AppRouteContext } from "../Routes";
|
||||
|
||||
const Container = styled.View`
|
||||
flex: 1;
|
||||
@@ -9,18 +14,57 @@ const Container = styled.View`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
const Version = styled.Text`
|
||||
text-align: center;
|
||||
color: lightgray;
|
||||
`;
|
||||
export const Version = () => {
|
||||
const showActionSheet = useActionSheet();
|
||||
const { resetRoute } = useContext(AppRouteContext);
|
||||
const { getInput, element } = useModalInput({
|
||||
title: "Password",
|
||||
autoCapitalize: "none",
|
||||
placeholder: "Aa123456",
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{element}
|
||||
<Text
|
||||
css={`
|
||||
text-align: center;
|
||||
color: lightgray;
|
||||
`}
|
||||
onLongPress={() => {
|
||||
showActionSheet(
|
||||
[
|
||||
{
|
||||
title: "Clear onboarding flat",
|
||||
onPress: async () => {
|
||||
await typedProfile.update({ onboardingCompleted: 0 });
|
||||
await logout();
|
||||
resetRoute?.();
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Promote to admin",
|
||||
onPress: () => {
|
||||
getInput(async (result) => {
|
||||
await promoteToAdmin(result);
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
{ defaultCancel: true }
|
||||
);
|
||||
}}
|
||||
>
|
||||
{hash.slice(0, 7)}-{code}
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UserPage = () => {
|
||||
return (
|
||||
<Container>
|
||||
<LogoutButton />
|
||||
<Version>
|
||||
{hash.slice(0, 7)}-{code}
|
||||
</Version>
|
||||
<Version />
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,46 +1,39 @@
|
||||
import React, { useContext } from "react";
|
||||
import { AppRouteContext } from "../../Routes";
|
||||
import { BigButton } from "../../../components/Button";
|
||||
import auth from "@react-native-firebase/auth";
|
||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||
import { typedPrivateProfile } from "../../../functions/user";
|
||||
import { keyOf } from "../../../functions/firebase/firestoreHooks";
|
||||
import { PrivateProfile } from "../../../functions/types";
|
||||
import DeviceInfo from "react-native-device-info";
|
||||
import firestore from "@react-native-firebase/firestore";
|
||||
import { logout } from "../../../functions/user";
|
||||
import { useActionSheet } from "../../../functions/actionsheet";
|
||||
import { StyleProp, ViewStyle } from "react-native";
|
||||
|
||||
const LogoutButton = () => {
|
||||
const LogoutButton = (props: { style?: StyleProp<ViewStyle> }) => {
|
||||
const { resetRoute } = useContext(AppRouteContext);
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
const showActionSheetWithOptions = useActionSheet();
|
||||
return (
|
||||
<BigButton
|
||||
style={props.style}
|
||||
onPress={() => {
|
||||
showActionSheetWithOptions(
|
||||
[
|
||||
{
|
||||
title: "Logout",
|
||||
type: "destructive",
|
||||
onPress: async () => {
|
||||
await logout();
|
||||
resetRoute?.();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "cancel",
|
||||
title: "Cancel",
|
||||
},
|
||||
],
|
||||
{
|
||||
title: "Are you sure?",
|
||||
options: ["Logout", "Cancel"],
|
||||
destructiveButtonIndex: 0,
|
||||
cancelButtonIndex: 1,
|
||||
},
|
||||
async (i) => {
|
||||
if (i === 1) {
|
||||
return;
|
||||
}
|
||||
await typedPrivateProfile
|
||||
.ref()
|
||||
.update(
|
||||
keyOf<PrivateProfile>("pushTokens") +
|
||||
"." +
|
||||
DeviceInfo.getUniqueId(),
|
||||
firestore.FieldValue.delete()
|
||||
);
|
||||
await auth().signOut();
|
||||
resetRoute?.();
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
Sign Out
|
||||
</BigButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DefaultTheme, Theme } from "react-native-paper";
|
||||
import { DefaultTheme as NavigationTheme } from "@react-navigation/native";
|
||||
|
||||
const theme: Theme = {
|
||||
...DefaultTheme,
|
||||
@@ -8,3 +9,12 @@ const theme: Theme = {
|
||||
};
|
||||
|
||||
export default theme;
|
||||
|
||||
export const navigationTheme = {
|
||||
...NavigationTheme,
|
||||
colors: {
|
||||
...NavigationTheme.colors,
|
||||
background: "white",
|
||||
tintColor: "#752aff",
|
||||
},
|
||||
};
|
||||
|
||||
22
app/src/functions/payment.ts
Normal file
22
app/src/functions/payment.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import stripe from "tipsi-stripe";
|
||||
import * as config from "../app.json";
|
||||
import functions from "@react-native-firebase/functions";
|
||||
import { AddPaymentMethodInput } from "../booster/functions/types";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
export const addPaymentFromStripe = async () => {
|
||||
stripe.setOptions({
|
||||
publishableKey: config.stripePublishKeys,
|
||||
});
|
||||
try {
|
||||
const { tokenId } = await stripe.paymentRequestWithCardForm({} as any);
|
||||
await functions().httpsCallable("payments-addPayment")({
|
||||
stripeToken: tokenId,
|
||||
} as AddPaymentMethodInput);
|
||||
} catch (e) {
|
||||
if (e.code === "cancelled") {
|
||||
return;
|
||||
}
|
||||
Alert.alert(e.message);
|
||||
}
|
||||
};
|
||||
19
app/src/functions/useOnPromise.ts
Normal file
19
app/src/functions/useOnPromise.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Alert } from "react-native";
|
||||
import { useState } from "react";
|
||||
|
||||
export const useOnPromise = (promise: () => Promise<any>) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
return {
|
||||
loading,
|
||||
onPress: async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await promise();
|
||||
setLoading(false);
|
||||
} catch (e) {
|
||||
Alert.alert(e.message);
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -11,7 +11,7 @@ service cloud.firestore {
|
||||
return userLoggedIn() && request.auth.uid in userIds
|
||||
}
|
||||
function isAdmin() {
|
||||
return false
|
||||
return userLoggedIn() && request.auth.token.isAdmin == true;
|
||||
}
|
||||
|
||||
// User profiles
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node"
|
||||
testEnvironment: "node",
|
||||
setupFilesAfterEnv: ["./jest.setup.js"],
|
||||
};
|
||||
|
||||
8
functions/jest.setup.js
Normal file
8
functions/jest.setup.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const admin = require("firebase-admin");
|
||||
const adminSDK = require("./src/adminSDK.json");
|
||||
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(adminSDK),
|
||||
});
|
||||
|
||||
jest.setTimeout(30 * 1000);
|
||||
@@ -17,11 +17,13 @@
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"@slack/webhook": "^5.0.3",
|
||||
"date-fns": "^2.12.0",
|
||||
"firebase-admin": "^8.9.0",
|
||||
"firebase-functions": "^3.3.0",
|
||||
"md5": "^2.2.1",
|
||||
"node-fetch": "^2.6.0",
|
||||
"sharp": "^0.25.2"
|
||||
"sharp": "^0.25.2",
|
||||
"stripe": "^8.44.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^25.1.3",
|
||||
@@ -33,9 +35,9 @@
|
||||
"@typescript-eslint/parser": "^2.14.0",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-config-typescript": "^3.0.0",
|
||||
"jest": "^25.1.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"typescript": "^3.7.2"
|
||||
"jest": "^25.3.0",
|
||||
"ts-jest": "^25.4.0",
|
||||
"typescript": "^3.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "10"
|
||||
|
||||
@@ -3,12 +3,12 @@ import { firestore } from "firebase-functions";
|
||||
import * as admin from "firebase-admin";
|
||||
import { Conversation, Message, UserStatus } from "../types";
|
||||
import { sendNotificationsTo } from "./utils/notifications";
|
||||
import { assertAuth, assertString, now } from "./utils/utils";
|
||||
import { assertNotNull, assertString, now } from "./utils/utils";
|
||||
import { useUserProfile } from "./utils/profiles";
|
||||
|
||||
export const startConversation = functions.https.onCall(
|
||||
async (data, context) => {
|
||||
assertAuth(context.auth);
|
||||
assertNotNull(context.auth);
|
||||
assertString(data.target);
|
||||
|
||||
const conversation: Conversation = {
|
||||
@@ -18,16 +18,13 @@ export const startConversation = functions.https.onCall(
|
||||
userIds: [context.auth.uid, data.target],
|
||||
users: {
|
||||
[context.auth.uid]: await useUserProfile(context.auth.uid).read(),
|
||||
[data.target]: await useUserProfile(data.target).read()
|
||||
[data.target]: await useUserProfile(data.target).read(),
|
||||
},
|
||||
available: true
|
||||
available: true,
|
||||
};
|
||||
const added = await admin
|
||||
.firestore()
|
||||
.collection("chats")
|
||||
.add(conversation);
|
||||
const added = await admin.firestore().collection("chats").add(conversation);
|
||||
return {
|
||||
id: added.id
|
||||
id: added.id,
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -40,26 +37,20 @@ export const onMessageCreate = firestore
|
||||
|
||||
const update: Partial<Conversation> = {
|
||||
updatedAt: message.createdAt,
|
||||
lastMessage: message
|
||||
lastMessage: message,
|
||||
};
|
||||
|
||||
const chatRef = admin
|
||||
.firestore()
|
||||
.collection("chats")
|
||||
.doc(chatId);
|
||||
const chatRef = admin.firestore().collection("chats").doc(chatId);
|
||||
|
||||
await chatRef.update(update);
|
||||
|
||||
const chat: Conversation = (await chatRef.get()).data() as any;
|
||||
|
||||
for (const targetUserId of chat.userIds.filter(
|
||||
id => id !== message.createdBy
|
||||
(id) => id !== message.createdBy
|
||||
)) {
|
||||
const userStatus: UserStatus = (
|
||||
await admin
|
||||
.database()
|
||||
.ref(`userStatus/${targetUserId}`)
|
||||
.once("value")
|
||||
await admin.database().ref(`userStatus/${targetUserId}`).once("value")
|
||||
).val();
|
||||
if (
|
||||
userStatus &&
|
||||
@@ -81,12 +72,12 @@ export const onMessageCreate = firestore
|
||||
0
|
||||
);
|
||||
await conversationCountsRef.update({
|
||||
[chatId]: (existingCounts[chatId] ?? 0) + 1
|
||||
[chatId]: (existingCounts[chatId] ?? 0) + 1,
|
||||
});
|
||||
await sendNotificationsTo(targetUserId, {
|
||||
title: message.user.name,
|
||||
body: message.content,
|
||||
badge: currentUnreadCount + 1
|
||||
badge: currentUnreadCount + 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
35
functions/src/booster/payment/payment.test.ts
Normal file
35
functions/src/booster/payment/payment.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { _addPayments, _deletePayment, _pay, _requestRefund } from "./payment";
|
||||
import { collection } from "../utils/firestore";
|
||||
|
||||
const testUser = "testUser";
|
||||
|
||||
it("should payments", async function () {
|
||||
await _addPayments("tok_visa", testUser);
|
||||
const payments = await collection("userReadonlyProfiles")
|
||||
.doc(testUser)
|
||||
.collection("payments")
|
||||
.get();
|
||||
console.log(payments.docs.map((p) => p.data()));
|
||||
await _deletePayment(payments.docs[0].id, testUser);
|
||||
});
|
||||
|
||||
it("should be able to charge", async function () {
|
||||
const payments = await collection("userReadonlyProfiles")
|
||||
.doc(testUser)
|
||||
.collection("payments")
|
||||
.get();
|
||||
|
||||
const paymentId = payments.docs[0].get("id");
|
||||
console.log(
|
||||
await _pay({
|
||||
uid: testUser,
|
||||
description: "Unit Test",
|
||||
paymentId,
|
||||
amountInCents: 50,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should be able to refund", async function () {
|
||||
await _requestRefund("ch_1GaMS3E5qqQ2Pr0jBOBWq6NH");
|
||||
});
|
||||
97
functions/src/booster/payment/payment.ts
Normal file
97
functions/src/booster/payment/payment.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import Stripe from "stripe";
|
||||
import { firestore, https } from "firebase-functions";
|
||||
import { HttpsError } from "firebase-functions/lib/providers/https";
|
||||
import {
|
||||
usePayments,
|
||||
usePrivateProfile,
|
||||
useReadonlyProfile,
|
||||
} from "../utils/profiles";
|
||||
import { AddPaymentMethodInput } from "../../types";
|
||||
import * as admin from "firebase-admin";
|
||||
import { assertNotNull } from "../utils/utils";
|
||||
import * as configs from "../../server.json";
|
||||
|
||||
const stripe = new Stripe(configs.stripeKey, {
|
||||
apiVersion: "2020-03-02",
|
||||
});
|
||||
|
||||
async function createCustomer(asUser: string): Promise<string> {
|
||||
const customer = await stripe.customers.create({
|
||||
description: asUser,
|
||||
});
|
||||
await useReadonlyProfile(asUser).update({
|
||||
stripeCustomerId: customer.id,
|
||||
});
|
||||
return customer.id;
|
||||
}
|
||||
|
||||
export const _addPayments = async (token: string, asUser: string) => {
|
||||
const profile = await useReadonlyProfile(asUser).read();
|
||||
const stripeCustomerId =
|
||||
profile.stripeCustomerId ?? (await createCustomer(asUser));
|
||||
const source = await stripe.customers.createSource(stripeCustomerId, {
|
||||
source: token,
|
||||
});
|
||||
await usePayments(asUser, source.id).update({
|
||||
...source,
|
||||
addedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
await usePrivateProfile(asUser).update({ defaultPayment: source as any });
|
||||
};
|
||||
|
||||
export const addPayment = https.onCall(
|
||||
async (data: AddPaymentMethodInput, context) => {
|
||||
assertNotNull(context.auth);
|
||||
console.log(
|
||||
`User ${
|
||||
context.auth.uid
|
||||
} add payment method with token starts with ${data.stripeToken.slice(
|
||||
0,
|
||||
5
|
||||
)}`
|
||||
);
|
||||
await _addPayments(data.stripeToken, context.auth.uid);
|
||||
}
|
||||
);
|
||||
|
||||
export const _deletePayment = async (paymentId: string, asUser: string) => {
|
||||
const profile = await useReadonlyProfile(asUser).read();
|
||||
const privateProfile = await usePrivateProfile(asUser).read();
|
||||
if (profile.stripeCustomerId == null) {
|
||||
throw new HttpsError(
|
||||
"failed-precondition",
|
||||
"User does not have customer id associated"
|
||||
);
|
||||
}
|
||||
await stripe.customers.deleteSource(profile.stripeCustomerId, paymentId);
|
||||
if (privateProfile.defaultPayment?.id === paymentId) {
|
||||
await usePrivateProfile(asUser).update({ defaultPayment: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
export const onDeletePayment = firestore
|
||||
.document("/userReadonlyProfiles/{userId}/payments/{paymentId}")
|
||||
.onDelete(async (snapshot, context) => {
|
||||
const { paymentId, userId } = context.params;
|
||||
console.log(`User ${userId} deleted source ${paymentId}`);
|
||||
await _deletePayment(paymentId, userId);
|
||||
});
|
||||
|
||||
export const _pay = async (props: {
|
||||
uid: string;
|
||||
paymentId: string;
|
||||
amountInCents: number;
|
||||
description: string;
|
||||
}) => {
|
||||
return await stripe.charges.create({
|
||||
amount: props.amountInCents,
|
||||
currency: "usd",
|
||||
customer: (await useReadonlyProfile(props.uid).read()).stripeCustomerId,
|
||||
source: props.paymentId,
|
||||
description: props.description,
|
||||
});
|
||||
};
|
||||
|
||||
export const _requestRefund = async (chargeId: string) => {
|
||||
return await stripe.refunds.create({ charge: chargeId });
|
||||
};
|
||||
@@ -2,10 +2,14 @@ import * as functions from "firebase-functions";
|
||||
import {
|
||||
usePrivateProfile,
|
||||
useReadonlyProfile,
|
||||
useUserProfile
|
||||
useUserProfile,
|
||||
} from "./utils/profiles";
|
||||
import { promoteToAdminPassword } from "../server.json";
|
||||
import { assertNotNull, assertString } from "./utils/utils";
|
||||
import { HttpsError } from "firebase-functions/lib/providers/https";
|
||||
import * as admin from "firebase-admin";
|
||||
|
||||
export const onUserCreate = functions.auth.user().onCreate(async user => {
|
||||
export const onUserCreate = functions.auth.user().onCreate(async (user) => {
|
||||
console.log(
|
||||
`User created: ${user.uid} from ${JSON.stringify(user.providerData)}`
|
||||
);
|
||||
@@ -13,22 +17,34 @@ export const onUserCreate = functions.auth.user().onCreate(async user => {
|
||||
await useUserProfile(user.uid).update({
|
||||
name: user.displayName,
|
||||
avatar: user.photoURL,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
await usePrivateProfile(user.uid).update({
|
||||
phone: user.phoneNumber
|
||||
phone: user.phoneNumber,
|
||||
});
|
||||
|
||||
await useReadonlyProfile(user.uid).update({
|
||||
accountBalance: 0,
|
||||
behaviorScore: 0
|
||||
behaviorScore: 0,
|
||||
});
|
||||
});
|
||||
|
||||
export const onUserDelete = functions.auth.user().onDelete(async user => {
|
||||
export const onUserDelete = functions.auth.user().onDelete(async (user) => {
|
||||
// Clean up user
|
||||
await useUserProfile(user.uid).delete();
|
||||
await usePrivateProfile(user.uid).delete();
|
||||
await useReadonlyProfile(user.uid).delete();
|
||||
});
|
||||
|
||||
export const promoteToAdmin = functions.https.onCall(async (data, context) => {
|
||||
assertNotNull(context.auth);
|
||||
const { password } = data;
|
||||
assertString(password);
|
||||
if (password !== promoteToAdminPassword) {
|
||||
throw new HttpsError("permission-denied", "wrong password");
|
||||
}
|
||||
await admin.auth().setCustomUserClaims(context.auth.uid, {
|
||||
isAdmin: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,35 +3,65 @@ import * as admin from "firebase-admin";
|
||||
export const collection = (collectionId: string) =>
|
||||
admin.firestore().collection(collectionId);
|
||||
|
||||
export function useDocAsType<T>() {
|
||||
return function(doc: admin.firestore.DocumentReference) {
|
||||
async function read() {
|
||||
const snapshot = await doc.get();
|
||||
const profile: T = snapshot.data() as any;
|
||||
if (!snapshot.exists || profile == null) {
|
||||
const error = new Error(`Doc ${doc.path} does not exist`);
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
return profile;
|
||||
function replaceUndefinedWithNull<
|
||||
T extends {
|
||||
[key: string]: any;
|
||||
}
|
||||
>(input: T): T {
|
||||
Object.keys(input).forEach((key) => {
|
||||
if (input[key] == null) {
|
||||
input[key as keyof T] = null as any;
|
||||
} else if (typeof input[key] === "object") {
|
||||
replaceUndefinedWithNull(input[key]);
|
||||
}
|
||||
async function update(newValue: Partial<T>) {
|
||||
return await doc.set(newValue, { merge: true });
|
||||
});
|
||||
return input;
|
||||
}
|
||||
|
||||
export function useDocAsType<T>(doc: admin.firestore.DocumentReference) {
|
||||
async function read() {
|
||||
const snapshot = await doc.get();
|
||||
const profile: T = snapshot.data() as any;
|
||||
if (!snapshot.exists || profile == null) {
|
||||
const error = new Error(`Doc ${doc.path} does not exist`);
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
async function listen(callback: (value: T) => void) {
|
||||
return doc.onSnapshot(snapshot => {
|
||||
callback(snapshot.data() as any);
|
||||
});
|
||||
}
|
||||
async function deleteDoc() {
|
||||
await doc.delete();
|
||||
}
|
||||
return {
|
||||
ref: doc,
|
||||
read,
|
||||
update,
|
||||
listen,
|
||||
delete: deleteDoc
|
||||
};
|
||||
return profile;
|
||||
}
|
||||
async function set(newValue: T): Promise<string> {
|
||||
await doc.set(replaceUndefinedWithNull(newValue));
|
||||
return doc.id;
|
||||
}
|
||||
async function update(newValue: Partial<T>) {
|
||||
return await doc.set(replaceUndefinedWithNull(newValue), { merge: true });
|
||||
}
|
||||
async function listen(callback: (value: T) => void) {
|
||||
return doc.onSnapshot((snapshot) => {
|
||||
callback(snapshot.data() as any);
|
||||
});
|
||||
}
|
||||
async function deleteDoc() {
|
||||
await doc.delete();
|
||||
}
|
||||
return {
|
||||
ref: doc,
|
||||
read,
|
||||
update,
|
||||
set,
|
||||
listen,
|
||||
delete: deleteDoc,
|
||||
};
|
||||
}
|
||||
|
||||
export const useQueryAsType = <T>(query: admin.firestore.Query) => {
|
||||
async function read() {
|
||||
const { docs } = await query.get();
|
||||
return docs.map((d) => ({
|
||||
ref: d.ref,
|
||||
id: d.id,
|
||||
doc: d.data() as T,
|
||||
}));
|
||||
}
|
||||
return { read };
|
||||
};
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import { collection, useDocAsType } from "./firestore";
|
||||
import { compose } from "./utils";
|
||||
import { PrivateProfile, Profile, ReadonlyProfile } from "../../types";
|
||||
|
||||
export const getUserProfileRef = (uid: string) =>
|
||||
collection("userProfiles").doc(uid);
|
||||
export const useUserProfile = (uid: string) =>
|
||||
useDocAsType<Profile>(collection("userProfiles").doc(uid));
|
||||
|
||||
export const getPrivateProfileRef = (uid: string) =>
|
||||
collection("userPrivateProfiles").doc(uid);
|
||||
export const usePrivateProfile = (uid: string) =>
|
||||
useDocAsType<PrivateProfile>(collection("userPrivateProfiles").doc(uid));
|
||||
|
||||
export const getReadonlyProfileRef = (uid: string) =>
|
||||
collection("userReadonlyProfiles").doc(uid);
|
||||
export const useReadonlyProfile = (uid: string) =>
|
||||
useDocAsType<ReadonlyProfile>(collection("userReadonlyProfiles").doc(uid));
|
||||
|
||||
export const useUserProfile = compose(
|
||||
getUserProfileRef,
|
||||
useDocAsType<Profile>()
|
||||
);
|
||||
|
||||
export const usePrivateProfile = compose(
|
||||
getPrivateProfileRef,
|
||||
useDocAsType<PrivateProfile>()
|
||||
);
|
||||
|
||||
export const useReadonlyProfile = compose(
|
||||
getReadonlyProfileRef,
|
||||
useDocAsType<ReadonlyProfile>()
|
||||
);
|
||||
export const usePayments = (uid: string, paymentId: string) =>
|
||||
useDocAsType<any>(
|
||||
collection("userReadonlyProfiles")
|
||||
.doc(uid)
|
||||
.collection("payments")
|
||||
.doc(paymentId)
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { HttpsError } from "firebase-functions/lib/providers/https";
|
||||
|
||||
export const now = admin.firestore.FieldValue.serverTimestamp() as any;
|
||||
|
||||
export function assertAuth<T>(val: T): asserts val is NonNullable<T> {
|
||||
export function assertNotNull<T>(val: T): asserts val is NonNullable<T> {
|
||||
if (val === undefined || val === null) {
|
||||
throw new HttpsError("unauthenticated", "You are not authenticated");
|
||||
}
|
||||
@@ -16,5 +16,5 @@ export function assertString(val: any): asserts val is string {
|
||||
}
|
||||
|
||||
export function compose<A, B, C>(l: (a: A) => B, r: (b: B) => C): (a: A) => C {
|
||||
return a => r(l(a));
|
||||
return (a) => r(l(a));
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ const lazyFunctions = {
|
||||
status: () => require("./booster/status").statusCheck,
|
||||
user: () => require("./booster/user"),
|
||||
image: () => require("./booster/image"),
|
||||
chat: () => require("./booster/chat")
|
||||
chat: () => require("./booster/chat"),
|
||||
payments: () => require("./booster/payment/payment"),
|
||||
// profile: () => require("./profile"),
|
||||
// user: () => require("./user"),
|
||||
// exp: () => require("./exports"),
|
||||
@@ -17,7 +18,7 @@ const lazyFunctions = {
|
||||
|
||||
const functionName = process.env.FUNCTION_NAME;
|
||||
(Object.keys(lazyFunctions) as Array<keyof typeof lazyFunctions>).forEach(
|
||||
name => {
|
||||
(name) => {
|
||||
if (!functionName || functionName.startsWith(name)) {
|
||||
exports[name] = lazyFunctions[name]();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"skipLibCheck": true,
|
||||
"module": "commonjs",
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"outDir": "lib",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"channel": "mercy-f16ad"
|
||||
},
|
||||
"functionsRoot": "https://us-central1-mercy-f16ad.cloudfunctions.net",
|
||||
"projectId": "mercy-f16ad"
|
||||
"projectId": "mercy-f16ad",
|
||||
"promoteToAdminPassword": "Aa123456"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user