Skip to content

Commit

Permalink
Merge branch 'main' of https://github.com/Expensify/App into m53884
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobkim9881 committed Feb 22, 2025
2 parents 5dec1b7 + 0b6d1a0 commit c9e11ad
Show file tree
Hide file tree
Showing 171 changed files with 3,547 additions and 1,258 deletions.
26 changes: 23 additions & 3 deletions .github/workflows/verifyHybridApp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,26 @@ on:
- 'android/AndroidManifest.xml'
- 'ios/Podfile.lock'
- 'ios/project.pbxproj'
pull_request_target:
types: [opened, synchronize]
branches-ignore: [staging, production]
paths:
- '**.kt'
- '**.java'
- '**.swift'
- '**.mm'
- '**.h'
- '**.cpp'
- 'package.json'
- 'patches/**'
- 'android/build.gradle'
- 'android/AndroidManifest.xml'
- 'ios/Podfile.lock'
- 'ios/project.pbxproj'

permissions:
pull-requests: write
contents: read

concurrency:
group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-verify-main
Expand All @@ -26,7 +46,7 @@ jobs:
comment_on_fork:
name: Comment on all PRs that are forks
# Only run on pull requests that *are* a fork
if: ${{ github.event.pull_request.head.repo.fork }}
if: ${{ github.event.pull_request.head.repo.fork && github.event_name == 'pull_request_target' }}
runs-on: ubuntu-latest
steps:
- name: Comment on forks
Expand All @@ -39,7 +59,7 @@ jobs:
name: Verify Android HybridApp builds on main
runs-on: ubuntu-latest-xl
# Only run on pull requests that are *not* on a fork
if: ${{ !github.event.pull_request.head.repo.fork }}
if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down Expand Up @@ -80,7 +100,7 @@ jobs:
name: Verify iOS HybridApp builds on main
runs-on: macos-15-xlarge
# Only run on pull requests that are *not* on a fork
if: ${{ !github.event.pull_request.head.repo.fork }}
if: ${{ !github.event.pull_request.head.repo.fork && github.event_name == 'pull_request' }}
steps:
- name: Checkout
uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion Mobile-Expensify
85 changes: 85 additions & 0 deletions __mocks__/@pusher/pusher-websocket-react-native/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type {PusherEvent} from '@pusher/pusher-websocket-react-native';
import CONST from '@src/CONST';

type OnSubscriptionSucceeded = () => void;

type OnEvent = (event: PusherEvent) => void;

type ChannelCallbacks = {
onSubscriptionSucceeded: OnSubscriptionSucceeded;
onEvent: OnEvent;
};

type InitProps = {
onConnectionStateChange: (currentState: string, previousState: string) => void;
};

type SubscribeProps = {
channelName: string;
onEvent: OnEvent;
onSubscriptionSucceeded: OnSubscriptionSucceeded;
};

type UnsubscribeProps = {
channelName: string;
};

class MockedPusher {
static instance: MockedPusher | null = null;

channels = new Map<string, ChannelCallbacks>();

socketId = 'mock-socket-id';

connectionState: string = CONST.PUSHER.STATE.DISCONNECTED;

static getInstance() {
if (!MockedPusher.instance) {
MockedPusher.instance = new MockedPusher();
}
return MockedPusher.instance;
}

init({onConnectionStateChange}: InitProps) {
onConnectionStateChange(CONST.PUSHER.STATE.CONNECTED, CONST.PUSHER.STATE.DISCONNECTED);
return Promise.resolve();
}

connect() {
this.connectionState = CONST.PUSHER.STATE.CONNECTED;
return Promise.resolve();
}

disconnect() {
this.connectionState = CONST.PUSHER.STATE.DISCONNECTED;
this.channels.clear();
return Promise.resolve();
}

subscribe({channelName, onEvent, onSubscriptionSucceeded}: SubscribeProps) {
if (!this.channels.has(channelName)) {
this.channels.set(channelName, {onEvent, onSubscriptionSucceeded});
onSubscriptionSucceeded();
}
return Promise.resolve();
}

unsubscribe({channelName}: UnsubscribeProps) {
this.channels.delete(channelName);
}

trigger({channelName, eventName, data}: PusherEvent) {
this.channels.get(channelName)?.onEvent({channelName, eventName, data: data as Record<string, unknown>});
}

getChannel(channelName: string) {
return this.channels.get(channelName);
}

getSocketId() {
return Promise.resolve(this.socketId);
}
}

// eslint-disable-next-line import/prefer-default-export
export {MockedPusher as Pusher};
9 changes: 0 additions & 9 deletions __mocks__/pusher-js/react-native.ts

This file was deleted.

4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
versionCode 1009010200
versionName "9.1.2-0"
versionCode 1009010400
versionName "9.1.4-0"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@

import com.expensify.chat.R;
import com.expensify.chat.shortcutManagerModule.ShortcutManagerUtils;
import com.expensify.chat.customairshipextender.PayloadHandler;
import com.urbanairship.AirshipConfigOptions;
import com.urbanairship.json.JsonMap;
import com.urbanairship.json.JsonValue;
Expand Down Expand Up @@ -119,18 +120,31 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @
}

// Attempt to parse data and apply custom notification styling
if (message.containsKey(PAYLOAD_KEY)) {
try {
JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap();
if (payload.containsKey(ONYX_DATA_KEY)) {
Objects.requireNonNull(payload.get(ONYX_DATA_KEY)).isNull();
Log.d(TAG, "payload contains onxyData");
String alert = message.getExtra(PushMessage.EXTRA_ALERT);
applyMessageStyle(context, builder, payload, arguments.getNotificationId(), alert);
}
} catch (Exception e) {
Log.e(TAG, "Failed to parse conversation, falling back to default notification style. SendID=" + message.getSendId(), e);
if (!message.containsKey(PAYLOAD_KEY)) {
return builder;
}

try {
String rawPayload = message.getExtra(PAYLOAD_KEY);
if (rawPayload == null) {
Log.d(TAG, "Failed to parse payload - payload is empty. SendID=" + message.getSendId());
return builder;
}

PayloadHandler handler = new PayloadHandler();
String processedPayload = handler.processPayload(rawPayload);
JsonMap payload = JsonValue.parseString(processedPayload).optMap();
if (!payload.containsKey(ONYX_DATA_KEY)) {
Log.d(TAG, "Failed to process payload - no onyx data. SendID=" + message.getSendId());
return builder;
}

Objects.requireNonNull(payload.get(ONYX_DATA_KEY)).isNull();
Log.d(TAG, "payload contains onxyData");
String alert = message.getExtra(PushMessage.EXTRA_ALERT);
applyMessageStyle(context, builder, payload, arguments.getNotificationId(), alert);
} catch (Exception e) {
Log.e(TAG, "Failed to parse conversation, falling back to default notification style. SendID=" + message.getSendId(), e);
}

return builder;
Expand Down Expand Up @@ -207,7 +221,7 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil
String name = messageData.get("person").getList().get(0).getMap().get("text").getString();
String avatar = messageData.get("avatar").getString();
String accountID = Integer.toString(messageData.get("actorAccountID").getInt(-1));

// Use the formatted alert message from the backend. Otherwise fallback on the message in the Onyx data.
String message = alert != null ? alert : messageData.get("message").getList().get(0).getMap().get("text").getString();
String roomName = payload.get("roomName") == null ? "" : payload.get("roomName").getString("");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.expensify.chat.customairshipextender

import android.util.Base64
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.util.zip.GZIPInputStream

class PayloadHandler {
private val BUFFER_SIZE = 4096
private val GZIP_MAGIC = byteArrayOf(0x1f.toByte(), 0x8b.toByte())

fun processPayload(payloadString: String): String =
runCatching {
val decoded = Base64.decode(payloadString, Base64.DEFAULT)
if (!isGzipped(decoded)) error("Input not gzipped")
return decompressGzip(decoded)
}.getOrDefault(payloadString)

private fun isGzipped(decoded: ByteArray) =
decoded.size >= 2 && decoded[0] == GZIP_MAGIC[0] && decoded[1] == GZIP_MAGIC[1]

private fun decompressGzip(compressed: ByteArray): String {
ByteArrayInputStream(compressed).use { bis ->
GZIPInputStream(bis).use { gis ->
ByteArrayOutputStream().use { output ->
val buffer = ByteArray(BUFFER_SIZE)
var len: Int
while (gis.read(buffer).also { len = it } > 0) {
output.write(buffer, 0, len)
}

return output.toString("UTF-8")
}
}
}
}
}
77 changes: 77 additions & 0 deletions docs/articles/expensify-classic/expenses/Edit-expenses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
title: Edit Expenses
description: Learn how to edit expenses in Expensify, including restrictions and permissions.
---

You can edit expenses in Expensify to update details like category, description, or attendees. However, some fields have restrictions based on the expense type and report status.

# Edit an Expense

{% include selector.html values="desktop, mobile" %}

{% include option.html value="desktop" %}

1. Click the **Expenses** tab.
2. Select the expense you want to edit.
3. Click the field you want to change (e.g., category, description, attendees).
4. Make your changes and click **Save**.

{% include end-option.html %}

{% include option.html value="mobile" %}

1. Tap the **Expenses** tab.
2. Select the expense you want to edit.
3. Tap **More Options**.
4. Update the relevant fields and tap **Save**.

{% include end-option.html %}

{% include end-selector.html %}

# Expense Editing Rules

Editing restrictions apply based on expense type and report status.

## General Editing Rules
- **Category, description, attendees, and report assignment** can be edited by the expense owner, approvers, and Workspace Admins.
- **Amount** can be edited for most manually entered expenses, except for company card transactions.
- **Tag and billable status** can be updated as long as the report is in an editable state.

## Company Card Expenses
- **Amount cannot be edited** for expenses imported from a company card.
- **Category, tag, and billable status** can be edited if the report is in the Open or Processing state.
- **Receipt images** can be added or replaced at any time.

## Submitted and Approved Expenses
- **Submitted expenses** can only be edited by an approver or Workspace Admin.
- **Approved expenses** cannot be edited unless they are reopened.
- **Expenses in a Closed report** cannot be edited.

# Delete an Expense

Expenses can only be deleted by the submitter, and the report must be in the Open state.

1. Navigate to the **Expenses** tab.
2. Select the expense you want to delete.
3. Click **Delete** and confirm.

{% include info.html %}
If the report has been submitted, you must retract it before deleting an expense.
{% include end-info.html %}

# FAQ

## Who can edit an expense?
- **Expense owner**: Can edit expenses if the report is Open.
- **Approvers and Workspace Admins**: Can edit submitted expenses before final approval.
- **Finance teams**: May have additional permissions based on workspace settings.

## Why can’t I edit my expense amount?
Company card expenses have a fixed amount based on imported transaction data and cannot be changed.

## Can I edit an expense after it has been approved?
No, approved expenses cannot be edited unless the report is reopened.

## How do I update an expense in a submitted report?
If you need to edit an expense in a submitted report, contact an approver or Workspace Admin to reopen the report.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ You must be a Workspace Admin to manage this feature.

Once your Expensify Cards have been issued, you can monitor them and check your card’s current balance, remaining limit, and earned cash back.

1. Click your profile image or icon in the bottom left menu.
1. Click on **Settings** in the bottom left menu.
2. Scroll down and click **Workspaces** in the left menu.
3. Select the workspace that contains the desired Expensify Cards.
4. Click **Expensify Card** in the left menu. Here, you’ll see a list of all of the issued cards.
Expand Down

This file was deleted.

3 changes: 3 additions & 0 deletions docs/redirects.csv
Original file line number Diff line number Diff line change
Expand Up @@ -642,4 +642,7 @@ https://help.expensify.com/articles/expensify-classic/travel/Book-with-Expensify
https://help.expensify.com/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md,https://help.expensify.com/articles/new-expensify/travel/Configure-travel-policy-and-preferences
https://help.expensify.com/articles/expensify-classic/travel/Track-Travel-Analytics.md,https://help.expensify.com/articles/new-expensify/travel/Track-Travel-Analytics
https://help.expensify.com/articles/expensify-classic/connections/ADP,https://help.expensify.com/articles/expensify-classic/connections/Connect-to-ADP
https://help.expensify.com/articles/new-expensify/connect-credit-cards/company-cards/Commercial-feeds,https://help.expensify.com/articles/new-expensify/connect-credit-cards/Commercial-feeds
https://help.expensify.com/articles/new-expensify/connect-credit-cards/company-cards/Company-Card-Settings,https://help.expensify.com/articles/new-expensify/connect-credit-cards/Company-Card-Settings
https://help.expensify.com/articles/new-expensify/connect-credit-cards/company-cards/Direct-feeds,https://help.expensify.com/articles/new-expensify/connect-credit-cards/Direct-feeds
https://help.expensify.com/articles/expensify-classic/travel,https://help.expensify.com/new-expensify/hubs/travel/
4 changes: 2 additions & 2 deletions ios/NewExpensify/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>9.1.2</string>
<string>9.1.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
Expand All @@ -44,7 +44,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>9.1.2.0</string>
<string>9.1.4.0</string>
<key>FullStory</key>
<dict>
<key>OrgId</key>
Expand Down
4 changes: 2 additions & 2 deletions ios/NewExpensifyTests/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>9.1.2</string>
<string>9.1.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>9.1.2.0</string>
<string>9.1.4.0</string>
</dict>
</plist>
Loading

0 comments on commit c9e11ad

Please sign in to comment.