Expensify / App

Welcome to New Expensify: a complete re-imagination of financial collaboration, centered around chat. Help us build the next generation of Expensify by sharing feedback and contributing to the code.
https://new.expensify.com
MIT License
3.5k stars 2.85k forks source link

[Payment Due Oct 1][$250] [#fast-apis] Subscribe guides to a new presence pusher channel #47888

Closed MonilBhavsar closed 2 weeks ago

MonilBhavsar commented 2 months ago

Context:

Guides are agents who help new customers with onboarding.

We use Pusher to send real time updates. We have a private user channel here https://github.com/Expensify/App/blob/f7d8e4850e729493eb8bfb52efa1a49d0d344ded/src/CONST.ts#L1154 private-encrypted-user-accountID-<> that is used to receive various events in real time like onyx updates, user typing and more.

Problem

Pusher private channel is dynamic, or unique for each user as it contains an accountID. For example: A channel name could look like private-encrypted-user-accountID-5-b8645604fe014e82b765ca84b7df1f2e

At the server side, if we want to know how many guides are online at the current time, then we need to make an API call to each pusher channel and see if it is occupied or not. If we have 20 guides, then we make 20 API calls to check guide's presence, which is not efficient.

Solution

Make guides subscribe to a presence channel - activeGuides https://pusher.com/docs/channels/using_channels/presence-channels/

With this, a server only will have to make one API call to find all active guides or users subscribed to this channel.

If a user is a guide - i.e. have a team.expensify.com domain and belongs to a specific policyID, then we additionally subscribe guide to the the guides presence channel.

Like the private user channel, a subscription to the channel should indicate guide's presence in the app - They should be subscribed if they login and are active, and should be unsubscribed if they sign out.

We probably have most of the code and logic for the private user channel. We need to similarly subscribe guides to the new channel.

Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~01fe75f988a6386f2e
  • Upwork Job ID: 1826754279960805798
  • Last Price Increase: 2024-08-29
  • Automatic offers:
    • situchan | Reviewer | 103739965
Issue OwnerCurrent Issue Owner: @trjExpensify
melvin-bot[bot] commented 2 months ago

Job added to Upwork: https://www.upwork.com/jobs/~01fe75f988a6386f2e

melvin-bot[bot] commented 2 months ago

Triggered auto assignment to Contributor-plus team member for initial proposal review - @situchan (External)

Krishna2323 commented 2 months ago

Proposal

Please re-state the problem that we are trying to solve in this issue.

[#fast-apis] Subscribe guides to a new presence pusher channel

What is the root cause of that problem?

Improvement.

What changes do you think we should make in order to solve the problem?

What alternative solutions did you explore? (Optional)


Result

daledah commented 2 months ago

Edited by proposal-police: This proposal was edited at 2024-08-23 01:43:52 UTC.

Proposal

Please re-state the problem that we are trying to solve in this issue.

function unsubscribeToActiveGuides() { Pusher.unsubscribe(activeGuides); }

the ```eventName``` can be provided later. And in this case, we can skip the ```eventCallback``` because BE just needs to know if the guide is online or not.

2. Create a subscribe component.

- This component is named ```ActiveGuidesEventListener```, its responsibility is to subscribe to a active guides channel if the current user is a guide and belongs to a specific policyID. Same as what we did in component [UserTypingEventListener](https://github.com/Expensify/App/blob/194f197a0c52fc95f18fbf3aa87afd42954f8184/src/pages/home/report/UserTypingEventListener.tsx).
````javascript
function ActiveGuidesEventListener({session, allPoliciesEmployeeList}) {
    const flatternPoliciesEmployee = flatten(Object.values(allPoliciesEmployeeList));
    const didSubscribeToActiveGuides = useRef(false);

    useEffect(
        () => () => {
            const sessionEmail = session.email;
            const emailDomain = Str.extractEmailDomain(sessionEmail ?? '');
            if (didSubscribeToActiveGuides.current) {
                return;
            }
            if (emailDomain !== CONST.EMAIL.GUIDES_DOMAIN) {
                return;
            }
            if (some(flatternPoliciesEmployee, (user) => user.email === sessionEmail)) {
                didSubscribeToActiveGuides.current = true;
                subscribeToActiveGuides();
            }
        },

        [session, flatternPoliciesEmployee],
    );
    return null;
}

export default withOnyx({
    session: {
        key: ONYXKEYS.SESSION,
    },
    allPoliciesEmployeeList: {
        key: ONYXKEYS.COLLECTION.POLICY,
        selector: (policy) => policy.employeeList,
    },
})(ActiveGuidesEventListener);
  1. Use the above component in AuthScreens: https://github.com/Expensify/App/blob/194f197a0c52fc95f18fbf3aa87afd42954f8184/src/libs/Navigation/AppNavigator/AuthScreens.tsx#L526

            </View>
           {didPusherInit && <ActiveGuidesEventListener />}

    with didPusherInit is state:

    const [didPusherInit, setDidPusherInit] = useState(false);

    and we set it to true in here.

  2. We don't need to cleanup function because this line: https://github.com/Expensify/App/blob/194f197a0c52fc95f18fbf3aa87afd42954f8184/src/libs/Navigation/AppNavigator/AuthScreens.tsx#L344 already did that.

What alternative solutions did you explore? (Optional)

export default withOnyx({ user: { key: ONYXKEYS.USER, }, })(ActiveGuidesEventListener);


- We still need to keep using ```didPusherInit && <ActiveGuidesEventListener />``` as in main solution to make sure the ```Pusher.subscribe``` is called after the Pusher is init.
daledah commented 2 months ago

Proposal updated

daledah commented 2 months ago

@MonilBhavsar In the issue's description you said:

have a team.expensify.com domain and belong to a specific policyID

if user has team.expensify.com domain but does not belongs to specific workspace, should we subscribe to the presence pusher channel?

daledah commented 2 months ago

Proposal updated

MonilBhavsar commented 2 months ago

if user has team.expensify.com domain but does not belongs to specific workspace, should we subscribe to the presence pusher channel?

No, both conditions are required

MonilBhavsar commented 2 months ago

Thanks for the proposals! πŸš€ @situchan could you please take a look.

mariapeever commented 2 months ago

Proposal

Please re-state the problem that we are trying to solve in this issue.

[#fast-apis] Subscribe guides to a new presence pusher channel #47888

What is the root cause of that problem?

Guide users are subscribed to a private encrypted user channel only and not subscribed to a presence channel ('presence-activeGuides').

What changes do you think we should make in order to solve the problem?

If a user is a guide, i.e. their email is an expensify team email, add a Pusher subscription to a presence channel request (PusherUtils.subscribeToPresenceChannelEvent) to the subscribeToUserEvents function in User (called in AuthScreens after Pusher init).

Add a subscribeToPresenceChannelEvent function to Pusher Utils with a Pusher subscribe request. All guides are subscribed to the same presence channel. Subscription to a presence channel adds a members property to the pusher socket, which includes a members count.

https://github.com/Expensify/App/blob/a29977bb8490e231d28babac11c6dc8650d19de8/src/libs/Navigation/AppNavigator/AuthScreens.tsx#L254-L262

Add a PusherUtils.subscribeToPresenceChannelEvent request just after PusherUtils.subscribeToPrivateUserChannelEvent. https://github.com/Expensify/App/blob/a29977bb8490e231d28babac11c6dc8650d19de8/src/libs/actions/User.ts#L660-L674 Add a subscribeToPresenceChannelEvent function just after subscribeToPrivateUserChannelEvent. https://github.com/Expensify/App/blob/a29977bb8490e231d28babac11c6dc8650d19de8/src/libs/PusherUtils.ts#L29-L31 Include a Pusher.subscribe request in the subscribeToPresenceChannelEvent function with channel name 'presence-activeGuides'. https://github.com/Expensify/App/blob/a29977bb8490e231d28babac11c6dc8650d19de8/src/libs/PusherUtils.ts#L48

What alternative solutions did you explore? (Optional)

N/A

melvin-bot[bot] commented 2 months ago

@MonilBhavsar, @situchan Whoops! This issue is 2 days overdue. Let's get this updated quick!

situchan commented 2 months ago

Thanks for the proposals.

I think @Krishna2323's solution is the most simple, though the logic to check if user is guide or not is missing. And unsubscribe logic is also not clear.

@daledah introducing new component is fine but I doubt subscribeToActiveGuides() is called before pusher init is complete. https://github.com/Expensify/App/blob/a29977bb8490e231d28babac11c6dc8650d19de8/src/libs/Navigation/AppNavigator/AuthScreens.tsx#L254-L262

@mariapeever I don't see any meaningful difference from @Krishna2323's proposal.

daledah commented 2 months ago

@situchan

I doubt subscribeToActiveGuides() is called before pusher init is complete

have a team.expensify.com domain and belongs to a specific policyID

situchan commented 2 months ago

@daledah thanks. What is the benefit of introducing new ActiveGuidesEventListener? Can't we just subscribe after subscribeToUserEvents()?

daledah commented 2 months ago

Can't we just subscribe after subscribeToUserEvents()

As mentioned in the OP's description, we just want to subscribe to a new presence pusher channel if they have a team.expensify.com domain and belong to a specific policyID. If we subscribe after subscribeToUserEvents(), it will subscribe to presence channel even if user is not in any policy.

situchan commented 2 months ago

it will subscribe to presence channel even if user is not in any policy.

Ofc we'll check if user belongs to specific policy

daledah commented 2 months ago

Ofc we'll check if user belongs to specific policy

Yeah. If we subscribe after subscribeToUserEvents(), we cannot check if user belongs to specific policy, right?

Krishna2323 commented 2 months ago

@situchan, I think we are already disconnecting from the socket when the AuthScreen is unmounted, If we want we can explicitly from the channel. For checking if the user is a guide or not, we can use the email and compare it with CONST.EMAIL.GUIDES_DOMAIN and we will also make sure that the user belongs to a policy by getting the policy using PolicyUtils.getPolicy(policyID) and finding if the member is in the participants list or not.

https://github.com/Expensify/App/blob/d4d1c0cf1ff50ebe4b7cc7934f932ed723eeea06/src/libs/Navigation/AppNavigator/AuthScreens.tsx#L340

https://github.com/Expensify/App/blob/d4d1c0cf1ff50ebe4b7cc7934f932ed723eeea06/src/libs/actions/Session/index.ts#L698-L701

situchan commented 2 months ago

Ofc we'll check if user belongs to specific policy

Yeah. If we subscribe after subscribeToUserEvents(), we cannot check if user belongs to specific policy, right?

Makes sense. We may need to connect ONYXKEYS.COLLECTION.POLICY in AuthScreens

@Krishna2323 what about this concern?

Krishna2323 commented 2 months ago

@Krishna2323 what about https://github.com/Expensify/App/issues/47888#issuecomment-2310968947?

I'm not sure why we can't check if user belongs to specific policy, can you please help me understand? Can't we get the policy using useOnyx or usePolicy hook?

situchan commented 2 months ago

@Krishna2323 pusher init is called on AuthScreens component mount

situchan commented 2 months ago

@daledah @Krishna2323 please test your solutions and confirm working

MonilBhavsar commented 2 months ago

We can also make backend return a flag - isGuide, if we think we're comprising frontend performance by connecting onyx in AuthScreens. And also, we don't load all policies upfront, so it might be that a guide is not being correctly identified as guide and hence not subscribed to pusher channel

Krishna2323 commented 2 months ago

We can also make backend return a flag - isGuide, if we think we're comprising frontend performance by connecting onyx in AuthScreens. And also, we don't load all policies upfront, so it might be that a guide is not being correctly identified as guide and hence not subscribed to pusher channel

I think that would be the best option, we can just use the flag to subscribe to the new channel, if that could be done, we can subscribe just after subscribeToUserEvents.

daledah commented 2 months ago

@situchan I added an alternative solution based on @MonilBhavsar's comment

situchan commented 2 months ago

@MonilBhavsar isGuide will be added to session or user?

MonilBhavsar commented 2 months ago

user sounds more appropriate

daledah commented 2 months ago

@MonilBhavsar

we don't load all policies upfront, so it might be that a guide is not being correctly identified

We just need to check if the user belongs to at least one policy or not, so I don't think we need to load all policies. What do you think?

Also, I updated the proposal based on comment.

MonilBhavsar commented 2 months ago

We just need to check if the user belongs to at least one policy or not, so I don't think we need to load all policies. What do you think?

How would we check? I mean not all policies would be loaded in Onyx

Krishna2323 commented 2 months ago

@MonilBhavsar, do you think that personal details will be loaded? I think its the same case as policies. IMO, It would be more appropriate if we can add the flag to the session object, since we don't want to subscribe to any new ONYX collection.

daledah commented 2 months ago

How would we check? I mean not all policies would be loaded in Onyx

Assume user has 100 policies, but BE just returns 20 policies. So we can check if user belongs to at least one policy based on those 20 policies, no need for all 100 policies returned.

MonilBhavsar commented 2 months ago

So we can check if user belongs to at least one policy based on those 20 policies, no need for all 100 policies returned.

Yes, but if the guide policy is not returned in those 20 policies, then code can go wrong, no

MonilBhavsar commented 2 months ago

if we can add the flag to the session object

user sounds more better to me than session

Krishna2323 commented 2 months ago

user sounds more better to me than session

Then we have to subscribe to PERSONAL_DETAILS_LIST in the auth screen and I think personal details can also be empty like policies and we will be connecting to ONYX in AuthScreens which I think isn't desired. Have I misunderstood something?

situchan commented 2 months ago

Looks like the solution would be to

Just need to make sure that user is fetched correctly at the time of pusher init @Krishna2323 please confirm

MonilBhavsar commented 2 months ago

Why subscribe to PERSONAL_DETAILS_LIST?

Krishna2323 commented 2 months ago

@situchan, if we logout and login we only get isUsingExpensifyCard property in the user object but later it shows 4 properties.

image

I think we need to connect ONYKEYS.USER to AuthScreens and in the callback we can subscribe to the channel if the user is a guide.

    const hasCheckedUserIsGuide = useRef(false);

    Onyx.connect({
        key: ONYXKEYS.USER,
        callback: (user) => {
            if (user.isGuide === undefined || !user?.validated) {
                hasCheckedUserIsGuide.current = false;
                return;
            }
            hasCheckedUserIsGuide.current = true;
            if (user.isGuide) {
                subscribeToActiveGuides();
            }
        },
    });
situchan commented 2 months ago

@Krishna2323 are you connecting onyx inside component lifecycle?

Krishna2323 commented 2 months ago

@situchan, no, it's not inside component lifecycle.

situchan commented 2 months ago

@Krishna2323 please share your test branch. And also test yourself

Krishna2323 commented 2 months ago

@situchan, I have tested the solution, here is the test branch.

daledah commented 2 months ago

@situchan I updated my alternative solution to match comment.

Krishna2323 commented 2 months ago

@situchan, my solution does not check if the pusher is initialized or not, you can review @daledah's proposal instead. TBH I don't have much idea how the pusher events works πŸ˜…

situchan commented 2 months ago

@situchan, my solution does not check if the pusher is initialized or no

Thanks. Was about to say that πŸ˜„

situchan commented 2 months ago

@daledah now that all questions are answered, can you please clarify https://github.com/Expensify/App/issues/47888#issuecomment-2310955054?

daledah commented 2 months ago

What is the benefit of introducing new ActiveGuidesEventListener? Can't we just subscribe after subscribeToUserEvents()?

  1. Make sure we only call subscribeToUserEvents() if pushser is init by using {didPusherInit && <ActiveGuidesEventListener />}.

  2. Make sure the user data is always out-of-date.

situchan commented 2 months ago

I think we can go with @daledah's proposal. @MonilBhavsar all yours

MonilBhavsar commented 2 months ago

@daledah do we need server to provide isGuide or are we checking that in the frontend?

daledah commented 2 months ago

@MonilBhavsar, my proposal includes two solutions. For the main solution, we don't need to provide isGuide. However, for the alternative solution, we will need the isGuide flag returned by the backend via the user data, based on your suggestion:

We can also make the backend return a flag - isGuide.

Both of them work well.

melvin-bot[bot] commented 1 month ago

πŸ“£ It's been a week! Do we have any satisfactory proposals yet? Do we need to adjust the bounty for this issue? πŸ’Έ