Skip to content

Commit 863267a

Browse files
Merge branch 'main' into helpsite-netsuite-error-docs
2 parents 860e716 + e26dc41 commit 863267a

339 files changed

Lines changed: 8081 additions & 2974 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/ISSUE_TEMPLATE/OnboardOffboardExpertContributor.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ Which action do you wish to take for this team member (select one):
3434
### Ring0 Tasks
3535

3636
- [ ] If adding, add to the appropriate GitHub child team of [external-expert-contributors](https://github.com/orgs/Expensify/teams/external-expert-contributors/teams) (each agency must have its own child team)
37+
- [ ] If adding, create an [IdentityDot account](https://stackoverflowteams.com/c/expensify/questions/22914) for the contributor
3738
- [ ] If removing, remove from our organization [here](https://github.com/orgs/Expensify/people)
39+
- [ ] If removing, delete the contributor's [IdentityDot account](https://stackoverflowteams.com/c/expensify/questions/22914)

.github/scripts/createDocsRoutes.ts

Lines changed: 5 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -61,34 +61,13 @@ function toTitleCase(str: string): string {
6161
}
6262

6363
/**
64-
* Get title from article frontmatter if present, otherwise from filename
64+
* @param filename - The name of the file (path used for href)
65+
* @param order - Optional order from front matter
66+
* @param titleOverride - Optional display title (e.g. subfolder name: "Export-Errors" -> "Export Errors")
6567
*/
66-
function getTitleFromFrontMatter(filePath: string): string | undefined {
67-
try {
68-
const content = fs.readFileSync(filePath, 'utf8');
69-
const parts = content.split('---');
70-
if (parts.length < 2) return undefined;
71-
const frontmatter = yaml.load(parts[1]) as Record<string, unknown>;
72-
const title = frontmatter?.title;
73-
return typeof title === 'string' ? title : undefined;
74-
} catch {
75-
return undefined;
76-
}
77-
}
78-
79-
/**
80-
* @param filename - The name of the file
81-
* @param fullPath - Optional full path to read frontmatter title
82-
* Card title: use filename for error codes (e.g. NS0521 Sync Error), else frontmatter or toTitleCase
83-
*/
84-
function getArticleObj(filename: string, order?: number, fullPath?: string): Article {
68+
function getArticleObj(filename: string, order?: number, titleOverride?: string): Article {
8569
const href = filename.replace('.md', '');
86-
const fallbackTitle = toTitleCase(href.replaceAll('-', ' '));
87-
// Error-code filenames (e.g. NS0521-Sync-Error): display as "NS0521 Sync Error" from filename
88-
const isErrorCode = href.match(/^[A-Z]{2,}\d+/);
89-
const title = isErrorCode
90-
? href.replaceAll('-', ' ')
91-
: (fullPath && getTitleFromFrontMatter(fullPath)) || fallbackTitle;
70+
const title = titleOverride ? toTitleCase(titleOverride.replaceAll('-', ' ')) : toTitleCase(href.replaceAll('-', ' '));
9271
return {
9372
href,
9473
title,

.github/workflows/buildIOS.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,12 @@ jobs:
144144
op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore/${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}
145145
op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Share_Extension/${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}
146146
op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Notification_Service/${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}" --force --out-file ./${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}
147+
op read "op://${{ vars.OP_VAULT }}/OldApp_AppStore_Live_Activity_Extension/OldApp_AppStore_LiveActivityExtension.mobileprovision" --force --out-file ./OldApp_AppStore_LiveActivityExtension.mobileprovision
147148
else
148149
op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc/OldApp_AdHoc.mobileprovision" --force --out-file ./OldApp_AdHoc.mobileprovision
149150
op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Share_Extension/OldApp_AdHoc_Share_Extension.mobileprovision" --force --out-file ./OldApp_AdHoc_Share_Extension.mobileprovision
150151
op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Notification_Service/OldApp_AdHoc_Notification_Service.mobileprovision" --force --out-file ./OldApp_AdHoc_Notification_Service.mobileprovision
152+
op read "op://${{ vars.OP_VAULT }}/OldApp_AdHoc_Live_Activity_Extension/OldApp_AdHoc_LiveActivityExtension.mobileprovision" --force --out-file ./OldApp_AdHoc_LiveActivityExtension.mobileprovision
151153
fi
152154
op read "op://${{ vars.OP_VAULT }}/New Expensify Distribution Certificate/Certificates.p12" --force --out-file ./Certificates.p12
153155
@@ -189,6 +191,8 @@ jobs:
189191
<string>(OldApp) AdHoc: Share Extension</string>
190192
<key>com.expensify.expensifylite.adhoc.NotificationServiceExtension</key>
191193
<string>(OldApp) AdHoc: Notification Service</string>
194+
<key>com.expensify.expensifylite.adhoc.LiveActivityExtension</key>
195+
<string>(OldApp) AdHoc: LiveActivityExtension</string>
192196
</dict>
193197
</dict>
194198
</plist>
@@ -213,7 +217,7 @@ jobs:
213217
if [ "${{ inputs.variant }}" == "Release" ]; then
214218
echo 'PROFILES=[{"name":"${{ vars.APPLE_STORE_PROVISIONING_PROFILE_NAME }}","file":"./${{ vars.APPLE_STORE_PROVISIONING_PROFILE_FILE }}"},{"name":"${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_NAME }}","file":"./${{ vars.APPLE_SHARE_PROVISIONING_PROFILE_FILE }}"},{"name":"${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_NAME }}","file":"./${{ vars.APPLE_NOTIFICATION_PROVISIONING_PROFILE_FILE }}"}]' >> "$GITHUB_OUTPUT"
215219
else
216-
echo 'PROFILES=[{"name":"(OldApp) AdHoc","file":"./OldApp_AdHoc.mobileprovision"},{"name":"(OldApp) AdHoc: Share Extension","file":"./OldApp_AdHoc_Share_Extension.mobileprovision"},{"name":"(OldApp) AdHoc: Notification Service","file":"./OldApp_AdHoc_Notification_Service.mobileprovision"}]' >> "$GITHUB_OUTPUT"
220+
echo 'PROFILES=[{"name":"(OldApp) AdHoc","file":"./OldApp_AdHoc.mobileprovision"},{"name":"(OldApp) AdHoc: Share Extension","file":"./OldApp_AdHoc_Share_Extension.mobileprovision"},{"name":"(OldApp) AdHoc: Notification Service","file":"./OldApp_AdHoc_Notification_Service.mobileprovision"},{"name":"(OldApp) AdHoc: LiveActivityExtension","file":"./OldApp_AdHoc_LiveActivityExtension.mobileprovision"}]' >> "$GITHUB_OUTPUT"
217221
fi
218222
219223
- name: Rock Remote Build - iOS

Mobile-Expensify

__mocks__/@ua/react-native-airship.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {AirshipContact, AirshipPush, AirshipPushAndroid, AirshipPushIOS, AirshipRoot} from '@ua/react-native-airship';
1+
import type {AirshipContact, AirshipLiveActivityManager, AirshipPush, AirshipPushAndroid, AirshipPushIOS, AirshipRoot, AirshipRootIOS} from '@ua/react-native-airship';
22

33
// eslint-disable-next-line no-restricted-syntax
44
enum EventType {
@@ -69,11 +69,24 @@ const contact = jest.fn().mockImplementation(() => ({
6969
module: jest.fn(),
7070
}))() as AirshipContact;
7171

72+
const liveActivityManager = jest.fn().mockImplementation(() => ({
73+
list: jest.fn(() => Promise.resolve([])),
74+
listAll: jest.fn(() => Promise.resolve([])),
75+
start: jest.fn(() => Promise.resolve({id: 'mock-activity-id'})),
76+
update: jest.fn(() => Promise.resolve()),
77+
end: jest.fn(() => Promise.resolve()),
78+
}))() as AirshipLiveActivityManager;
79+
80+
const airshipIOS = jest.fn().mockImplementation(() => ({
81+
liveActivityManager,
82+
}))() as AirshipRootIOS;
83+
7284
const Airship: Partial<AirshipRoot> = {
7385
addListener: jest.fn(),
7486
removeAllListeners: jest.fn(),
7587
push,
7688
contact,
89+
iOS: airshipIOS,
7790
};
7891

7992
export default Airship;

android/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ android {
111111
minSdkVersion rootProject.ext.minSdkVersion
112112
targetSdkVersion rootProject.ext.targetSdkVersion
113113
multiDexEnabled rootProject.ext.multiDexEnabled
114-
versionCode 1009033106
115-
versionName "9.3.31-6"
114+
versionCode 1009033202
115+
versionName "9.3.32-2"
116116
// Supported language variants must be declared here to avoid from being removed during the compilation.
117117
// This also helps us to not include unnecessary language variants in the APK.
118118
resConfigs "en", "es"

android/app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
android:permission="android.permission.BIND_JOB_SERVICE"
3131
android:exported="false" />
3232

33+
<service
34+
android:name=".customairshipextender.GpsTripService"
35+
android:stopWithTask="false"
36+
android:exported="false" />
37+
3338
<activity
3439
android:name=".MainActivity"
3540
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"

android/app/src/main/java/com/expensify/chat/customairshipextender/CustomAirshipExtender.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.urbanairship.UAirship;
66
import com.urbanairship.push.NotificationListener;
77
import com.urbanairship.push.PushManager;
8+
import com.urbanairship.liveupdate.LiveUpdateManager;
89
import com.urbanairship.reactnative.AirshipExtender;
910

1011
public class CustomAirshipExtender implements AirshipExtender {
@@ -14,5 +15,7 @@ public void onAirshipReady(@NonNull Context context, @NonNull UAirship airship)
1415

1516
CustomNotificationProvider notificationProvider = new CustomNotificationProvider(context, airship.getAirshipConfigOptions());
1617
pushManager.setNotificationProvider(notificationProvider);
18+
19+
LiveUpdateManager.shared().register(GpsLiveUpdateHandler.TYPE, new GpsLiveUpdateHandler());
1720
}
1821
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.expensify.chat.customairshipextender
2+
3+
import android.app.NotificationChannel
4+
import android.app.NotificationManager
5+
import android.app.PendingIntent
6+
import android.content.Context
7+
import android.content.Intent
8+
import android.net.Uri
9+
import android.os.Build
10+
import androidx.core.app.NotificationCompat
11+
import com.expensify.chat.R
12+
import com.urbanairship.liveupdate.LiveUpdate
13+
import com.urbanairship.liveupdate.LiveUpdateEvent
14+
import com.urbanairship.liveupdate.LiveUpdateResult
15+
import com.urbanairship.liveupdate.SuspendLiveUpdateNotificationHandler
16+
17+
class GpsLiveUpdateHandler : SuspendLiveUpdateNotificationHandler() {
18+
19+
override suspend fun onUpdate(
20+
context: Context,
21+
event: LiveUpdateEvent,
22+
update: LiveUpdate
23+
): LiveUpdateResult<NotificationCompat.Builder> {
24+
if (event == LiveUpdateEvent.END) {
25+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
26+
context.stopService(Intent(context, GpsTripService::class.java))
27+
}
28+
return LiveUpdateResult.cancel()
29+
}
30+
31+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
32+
context.startService(Intent(context, GpsTripService::class.java))
33+
}
34+
ensureNotificationChannel(context)
35+
36+
val title = update.content.opt("title").optString() ?: "GPS tracking in progress"
37+
val body = update.content.opt("body").optString() ?: "Go to the app to finish"
38+
39+
val builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID)
40+
.setSmallIcon(R.drawable.ic_notification)
41+
.setContentTitle(title)
42+
.setContentText(body)
43+
.setPriority(NotificationCompat.PRIORITY_HIGH)
44+
.setCategory(NotificationCompat.CATEGORY_EVENT)
45+
.setAutoCancel(false)
46+
47+
val deepLink = update.content.opt("deepLink").optString()
48+
if (!deepLink.isNullOrEmpty()) {
49+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("new-expensify://$deepLink")).apply {
50+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
51+
}
52+
val pendingIntent = PendingIntent.getActivity(
53+
context,
54+
0,
55+
intent,
56+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
57+
)
58+
builder.setContentIntent(pendingIntent)
59+
}
60+
61+
return LiveUpdateResult.ok(builder)
62+
}
63+
64+
private fun ensureNotificationChannel(context: Context) {
65+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
66+
67+
val manager = context.getSystemService(NotificationManager::class.java)
68+
if (manager.getNotificationChannel(NOTIFICATION_CHANNEL_ID) != null) return
69+
70+
val channel = NotificationChannel(
71+
NOTIFICATION_CHANNEL_ID,
72+
NOTIFICATION_CHANNEL_NAME,
73+
NotificationManager.IMPORTANCE_HIGH
74+
).apply {
75+
description = NOTIFICATION_CHANNEL_DESCRIPTION
76+
}
77+
manager.createNotificationChannel(channel)
78+
}
79+
80+
companion object {
81+
const val NOTIFICATION_CHANNEL_ID = "gps_trip_notification_channel_id"
82+
const val NOTIFICATION_CHANNEL_NAME = "GPS Trip Tracking"
83+
const val NOTIFICATION_CHANNEL_DESCRIPTION = "Shows when a GPS distance trip is being tracked"
84+
const val TYPE = "gps_trip_notification"
85+
}
86+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.expensify.chat.customairshipextender
2+
3+
import android.app.NotificationManager
4+
import android.app.Service
5+
import android.content.Intent
6+
import android.os.IBinder
7+
8+
/**
9+
* Lightweight service that runs alongside the Airship LiveUpdate GPS notification.
10+
* Its sole purpose is to cancel the notification when the app is killed,
11+
* since Airship LiveUpdate notifications persist beyond the app process lifecycle.
12+
*/
13+
class GpsTripService : Service() {
14+
15+
override fun onBind(intent: Intent?): IBinder? = null
16+
17+
override fun onTaskRemoved(rootIntent: Intent?) {
18+
cancelGpsNotifications()
19+
stopSelf()
20+
super.onTaskRemoved(rootIntent)
21+
}
22+
23+
private fun cancelGpsNotifications() {
24+
val notificationManager = getSystemService(NotificationManager::class.java) ?: return
25+
notificationManager.activeNotifications
26+
.filter { it.notification.channelId == GpsLiveUpdateHandler.NOTIFICATION_CHANNEL_ID }
27+
.forEach { notificationManager.cancel(it.tag, it.id) }
28+
}
29+
}

0 commit comments

Comments
 (0)