diff --git a/ui/js/actions/user.js b/ui/js/actions/user.js
index 8a0e59ef1..e649bed88 100644
--- a/ui/js/actions/user.js
+++ b/ui/js/actions/user.js
@@ -2,6 +2,7 @@ import * as types from "constants/action_types";
import * as modals from "constants/modal_types";
import lbryio from "lbryio";
import { doOpenModal } from "actions/app";
+import { doOpenModal, doShowSnackBar } from "actions/app";
import { doRewardList, doClaimRewardType } from "actions/rewards";
import { selectEmailToVerify, selectUser } from "selectors/user";
import rewards from "rewards";
@@ -172,3 +173,61 @@ export function doFetchAccessToken() {
lbryio.getAuthToken().then(success);
};
}
+
+export function doFetchInviteStatus() {
+ return function(dispatch, getState) {
+ dispatch({
+ type: types.USER_INVITE_STATUS_FETCH_STARTED,
+ });
+
+ lbryio
+ .call("user", "invite_status")
+ .then(status => {
+ console.log(status);
+ dispatch({
+ type: types.USER_INVITE_STATUS_FETCH_SUCCESS,
+ data: {
+ invitesRemaining: status.invites_remaining,
+ invitees: status.invitees,
+ },
+ });
+ })
+ .catch(error => {
+ dispatch({
+ type: types.USER_INVITE_STATUS_FETCH_FAILURE,
+ data: { error },
+ });
+ });
+ };
+}
+
+export function doUserInviteNew(email) {
+ return function(dispatch, getState) {
+ dispatch({
+ type: types.USER_INVITE_NEW_STARTED,
+ });
+
+ lbryio
+ .call("user", "invite", { email: email }, "post")
+ .then(invite => {
+ dispatch({
+ type: types.USER_INVITE_NEW_SUCCESS,
+ data: { email },
+ });
+
+ dispatch(
+ doShowSnackBar({
+ message: __("Invite sent to %s", email),
+ })
+ );
+
+ dispatch(doFetchInviteStatus());
+ })
+ .catch(error => {
+ dispatch({
+ type: types.USER_INVITE_NEW_FAILURE,
+ data: { error },
+ });
+ });
+ };
+}
diff --git a/ui/js/component/inviteList/index.js b/ui/js/component/inviteList/index.js
new file mode 100644
index 000000000..3f7361454
--- /dev/null
+++ b/ui/js/component/inviteList/index.js
@@ -0,0 +1,16 @@
+import React from "react";
+import { connect } from "react-redux";
+import {
+ selectUserInvitees,
+ selectUserInviteStatusIsPending,
+} from "selectors/user";
+import InviteList from "./view";
+
+const select = state => ({
+ invitees: selectUserInvitees(state),
+ isPending: selectUserInviteStatusIsPending(state),
+});
+
+const perform = dispatch => ({});
+
+export default connect(select, perform)(InviteList);
diff --git a/ui/js/component/inviteList/view.jsx b/ui/js/component/inviteList/view.jsx
new file mode 100644
index 000000000..8d3d4962d
--- /dev/null
+++ b/ui/js/component/inviteList/view.jsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { Icon } from "component/common";
+
+class InviteList extends React.PureComponent {
+ render() {
+ const { invitees } = this.props;
+
+ if (!invitees) {
+ return null;
+ }
+
+ return (
+
+
+
{__("Invite History")}
+
+
+ {invitees.length === 0 &&
+
{__("You haven't invited anyone.")} }
+ {invitees.length > 0 &&
+
+
+
+
+ {__("Invitee Email")}
+ |
+
+ {__("Invite Status")}
+ |
+
+ {__("Reward")}
+ |
+
+
+
+ {invitees.map((invitee, index) => {
+ return (
+
+ {invitee.email} |
+
+ {invitee.invite_accepted && }
+ |
+
+ {invitee.invite_reward_claimed &&
+ }
+ |
+
+ );
+ })}
+
+
}
+
+
+ );
+ }
+}
+
+export default InviteList;
diff --git a/ui/js/component/inviteNew/index.js b/ui/js/component/inviteNew/index.js
new file mode 100644
index 000000000..b210aaf36
--- /dev/null
+++ b/ui/js/component/inviteNew/index.js
@@ -0,0 +1,21 @@
+import React from "react";
+import { connect } from "react-redux";
+import InviteNew from "./view";
+import {
+ selectUserInvitesRemaining,
+ selectUserInviteNewIsPending,
+ selectUserInviteNewErrorMessage,
+} from "selectors/user";
+import { doUserInviteNew } from "actions/user";
+
+const select = state => ({
+ errorMessage: selectUserInviteNewErrorMessage(state),
+ invitesRemaining: selectUserInvitesRemaining(state),
+ isPending: selectUserInviteNewIsPending(state),
+});
+
+const perform = dispatch => ({
+ inviteNew: email => dispatch(doUserInviteNew(email)),
+});
+
+export default connect(select, perform)(InviteNew);
diff --git a/ui/js/component/inviteNew/view.jsx b/ui/js/component/inviteNew/view.jsx
new file mode 100644
index 000000000..a02f0054b
--- /dev/null
+++ b/ui/js/component/inviteNew/view.jsx
@@ -0,0 +1,100 @@
+import React from "react";
+import { BusyMessage } from "component/common";
+import Link from "component/link";
+import { FormRow } from "component/form.js";
+
+class FormInviteNew extends React.PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ email: "",
+ };
+ }
+
+ handleEmailChanged(event) {
+ this.setState({
+ email: event.target.value,
+ });
+ }
+
+ handleSubmit(event) {
+ event.preventDefault();
+ this.props.inviteNew(this.state.email);
+ }
+
+ render() {
+ const { errorMessage, isPending } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+class InviteNew extends React.PureComponent {
+ render() {
+ const {
+ errorMessage,
+ invitesRemaining,
+ inviteNew,
+ inviteStatusIsPending,
+ isPending,
+ } = this.props;
+
+ return (
+
+
+
+ {__(
+ "Invite a Friend (or Enemy) (or Someone You Are Somewhat Ambivalent About)"
+ )}
+
+
+
+ {invitesRemaining > 0 &&
+
{__("You have %s invites remaining.", invitesRemaining)}
}
+ {invitesRemaining <= 0 &&
+
+
+ {__("You have no invites.", invitesRemaining)}
+
+
}
+
+ {!inviteStatusIsPending &&
+ invitesRemaining > 0 &&
+
+
+
}
+
+ );
+ }
+}
+
+export default InviteNew;
diff --git a/ui/js/component/router/view.jsx b/ui/js/component/router/view.jsx
index 4b1b20641..501b521a5 100644
--- a/ui/js/component/router/view.jsx
+++ b/ui/js/component/router/view.jsx
@@ -16,6 +16,7 @@ import FileListPublished from "page/fileListPublished";
import ChannelPage from "page/channel";
import SearchPage from "page/search";
import AuthPage from "page/auth";
+import InvitePage from "page/invite";
import BackupPage from "page/backup";
const route = (page, routesMap) => {
@@ -35,6 +36,7 @@ const Router = props => {
discover: ,
downloaded: ,
help: ,
+ invite: ,
publish: ,
published: ,
receive: ,
diff --git a/ui/js/component/userEmailNew/index.js b/ui/js/component/userEmailNew/index.js
index 4040606eb..699f794ec 100644
--- a/ui/js/component/userEmailNew/index.js
+++ b/ui/js/component/userEmailNew/index.js
@@ -1,6 +1,6 @@
import React from "react";
import { connect } from "react-redux";
-import { doUserEmailNew } from "actions/user";
+import { doUserEmailNew, doUserInviteNew } from "actions/user";
import {
selectEmailNewIsPending,
selectEmailNewErrorMessage,
diff --git a/ui/js/component/walletAddress/index.js b/ui/js/component/walletAddress/index.js
index ad0021f41..d5a8d7696 100644
--- a/ui/js/component/walletAddress/index.js
+++ b/ui/js/component/walletAddress/index.js
@@ -5,7 +5,7 @@ import {
selectReceiveAddress,
selectGettingNewAddress,
} from "selectors/wallet";
-import WalletPage from "./view";
+import WalletAddress from "./view";
const select = state => ({
receiveAddress: selectReceiveAddress(state),
@@ -17,4 +17,4 @@ const perform = dispatch => ({
getNewAddress: () => dispatch(doGetNewAddress()),
});
-export default connect(select, perform)(WalletPage);
+export default connect(select, perform)(WalletAddress);
diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js
index e44102c54..edb60621f 100644
--- a/ui/js/constants/action_types.js
+++ b/ui/js/constants/action_types.js
@@ -108,6 +108,15 @@ export const USER_IDENTITY_VERIFY_FAILURE = "USER_IDENTITY_VERIFY_FAILURE";
export const USER_FETCH_STARTED = "USER_FETCH_STARTED";
export const USER_FETCH_SUCCESS = "USER_FETCH_SUCCESS";
export const USER_FETCH_FAILURE = "USER_FETCH_FAILURE";
+export const USER_INVITE_STATUS_FETCH_STARTED =
+ "USER_INVITE_STATUS_FETCH_STARTED";
+export const USER_INVITE_STATUS_FETCH_SUCCESS =
+ "USER_INVITE_STATUS_FETCH_SUCCESS";
+export const USER_INVITE_STATUS_FETCH_FAILURE =
+ "USER_INVITE_STATUS_FETCH_FAILURE";
+export const USER_INVITE_NEW_STARTED = "USER_INVITE_NEW_STARTED";
+export const USER_INVITE_NEW_SUCCESS = "USER_INVITE_NEW_SUCCESS";
+export const USER_INVITE_NEW_FAILURE = "USER_INVITE_NEW_FAILURE";
export const FETCH_ACCESS_TOKEN_SUCCESS = "FETCH_ACCESS_TOKEN_SUCCESS";
// Rewards
diff --git a/ui/js/modal/modalCreditIntro/view.jsx b/ui/js/modal/modalCreditIntro/view.jsx
index 8a2b7b500..226ab0f2e 100644
--- a/ui/js/modal/modalCreditIntro/view.jsx
+++ b/ui/js/modal/modalCreditIntro/view.jsx
@@ -1,6 +1,6 @@
import React from "react";
import { Modal } from "modal/modal";
-import { CreditAmount } from "component/common";
+import { CreditAmount, CurrencySymbol } from "component/common";
import Link from "component/link/index";
const ModalCreditIntro = props => {
@@ -14,7 +14,7 @@ const ModalCreditIntro = props => {
{__("Claim Your Credits")}
The LBRY network is controlled and powered by credits called{" "}
- LBC, a blockchain asset.
+ , a blockchain asset.
{__("New patrons receive ")} {" "}
diff --git a/ui/js/page/invite/index.js b/ui/js/page/invite/index.js
new file mode 100644
index 000000000..c97cd1b29
--- /dev/null
+++ b/ui/js/page/invite/index.js
@@ -0,0 +1,19 @@
+import React from "react";
+import { connect } from "react-redux";
+import InvitePage from "./view";
+import { doFetchInviteStatus } from "actions/user";
+import {
+ selectUserInviteStatusFailed,
+ selectUserInviteStatusIsPending,
+} from "selectors/user";
+
+const select = state => ({
+ isFailed: selectUserInviteStatusFailed(state),
+ isPending: selectUserInviteStatusIsPending(state),
+});
+
+const perform = dispatch => ({
+ fetchInviteStatus: () => dispatch(doFetchInviteStatus()),
+});
+
+export default connect(select, perform)(InvitePage);
diff --git a/ui/js/page/invite/view.jsx b/ui/js/page/invite/view.jsx
new file mode 100644
index 000000000..0488ca1f3
--- /dev/null
+++ b/ui/js/page/invite/view.jsx
@@ -0,0 +1,32 @@
+import React from "react";
+import { BusyMessage } from "component/common";
+import SubHeader from "component/subHeader";
+import InviteNew from "component/inviteNew";
+import InviteList from "component/inviteList";
+
+class InvitePage extends React.PureComponent {
+ componentWillMount() {
+ this.props.fetchInviteStatus();
+ }
+
+ render() {
+ const { isPending, isFailed } = this.props;
+
+ return (
+
+
+ {isPending &&
+ }
+ {!isPending &&
+ isFailed &&
+
+ {__("Failed to retrieve invite status.")}
+ }
+ {!isPending && !isFailed && }
+ {!isPending && !isFailed && }
+
+ );
+ }
+}
+
+export default InvitePage;
diff --git a/ui/js/page/receiveCredits/view.jsx b/ui/js/page/receiveCredits/view.jsx
index 11e0e0690..b00f8dc34 100644
--- a/ui/js/page/receiveCredits/view.jsx
+++ b/ui/js/page/receiveCredits/view.jsx
@@ -7,7 +7,6 @@ const ReceiveCreditsPage = props => {
return (
-
);
diff --git a/ui/js/page/sendCredits/view.jsx b/ui/js/page/sendCredits/view.jsx
index 06e8ed5ce..b0603b9c7 100644
--- a/ui/js/page/sendCredits/view.jsx
+++ b/ui/js/page/sendCredits/view.jsx
@@ -7,7 +7,6 @@ const SendCreditsPage = props => {
return (
-
);
diff --git a/ui/js/reducers/user.js b/ui/js/reducers/user.js
index 02203cdfe..bc19f3464 100644
--- a/ui/js/reducers/user.js
+++ b/ui/js/reducers/user.js
@@ -8,6 +8,11 @@ const defaultState = {
emailNewIsPending: false,
emailNewErrorMessage: "",
emailToVerify: "",
+ inviteNewErrorMessage: "",
+ inviteNewIsPending: false,
+ inviteStatusIsPending: false,
+ invitesRemaining: undefined,
+ invitees: undefined,
user: undefined,
};
@@ -142,6 +147,49 @@ reducers[types.FETCH_ACCESS_TOKEN_SUCCESS] = function(state, action) {
});
};
+reducers[types.USER_INVITE_STATUS_FETCH_STARTED] = function(state, action) {
+ return Object.assign({}, state, {
+ inviteStatusIsPending: true,
+ });
+};
+
+reducers[types.USER_INVITE_STATUS_FETCH_SUCCESS] = function(state, action) {
+ return Object.assign({}, state, {
+ inviteStatusIsPending: false,
+ invitesRemaining: action.data.invitesRemaining,
+ invitees: action.data.invitees,
+ });
+};
+
+reducers[types.USER_INVITE_NEW_STARTED] = function(state, action) {
+ return Object.assign({}, state, {
+ inviteNewIsPending: true,
+ inviteNewErrorMessage: "",
+ });
+};
+
+reducers[types.USER_INVITE_NEW_SUCCESS] = function(state, action) {
+ return Object.assign({}, state, {
+ inviteNewIsPending: false,
+ inviteNewErrorMessage: "",
+ });
+};
+
+reducers[types.USER_INVITE_NEW_FAILURE] = function(state, action) {
+ return Object.assign({}, state, {
+ inviteNewIsPending: false,
+ inviteNewErrorMessage: action.data.error.message,
+ });
+};
+
+reducers[types.USER_INVITE_STATUS_FETCH_FAILURE] = function(state, action) {
+ return Object.assign({}, state, {
+ inviteStatusIsPending: false,
+ invitesRemaining: null,
+ invitees: null,
+ });
+};
+
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js
index 1ad6a81b9..959c80c99 100644
--- a/ui/js/selectors/app.js
+++ b/ui/js/selectors/app.js
@@ -138,12 +138,14 @@ export const selectHeaderLinks = createSelector(selectCurrentPage, page => {
case "wallet":
case "send":
case "receive":
+ case "invite":
case "rewards":
case "backup":
return {
wallet: __("Overview"),
send: __("Send"),
receive: __("Receive"),
+ invite: __("Invites"),
rewards: __("Rewards"),
};
case "downloaded":
diff --git a/ui/js/selectors/user.js b/ui/js/selectors/user.js
index a499e5666..43ac07413 100644
--- a/ui/js/selectors/user.js
+++ b/ui/js/selectors/user.js
@@ -68,3 +68,33 @@ export const selectAccessToken = createSelector(
_selectState,
state => state.accessToken
);
+
+export const selectUserInviteStatusIsPending = createSelector(
+ _selectState,
+ state => state.inviteStatusIsPending
+);
+
+export const selectUserInvitesRemaining = createSelector(
+ _selectState,
+ state => state.invitesRemaining
+);
+
+export const selectUserInvitees = createSelector(
+ _selectState,
+ state => state.invitees
+);
+
+export const selectUserInviteStatusFailed = createSelector(
+ selectUserInvitesRemaining,
+ inviteStatus => selectUserInvitesRemaining === null
+);
+
+export const selectUserInviteNewIsPending = createSelector(
+ _selectState,
+ state => state.inviteNewIsPending
+);
+
+export const selectUserInviteNewErrorMessage = createSelector(
+ _selectState,
+ state => state.inviteNewErrorMessage
+);
diff --git a/ui/scss/component/_table.scss b/ui/scss/component/_table.scss
index 9d60cf6e8..7f5600aa1 100644
--- a/ui/scss/component/_table.scss
+++ b/ui/scss/component/_table.scss
@@ -24,6 +24,9 @@ table.table-standard {
img {
vertical-align: text-bottom;
}
+ &.text-center {
+ text-align: center;
+ }
}
tr.thead:not(:first-child) th {
border-top: 1px solid #e2e2e2;