Compare commits

..

28 Commits

Author SHA1 Message Date
vishnukvmd
1e1633bb45 Merge branch 'main' into f-droid 2024-03-13 21:57:19 +05:30
Vishnu Mohandas
ba5686a07a v0.8.71 (#1090) 2024-03-13 21:57:05 +05:30
vishnukvmd
1cacefa1fd v0.8.71 2024-03-13 21:56:48 +05:30
vishnukvmd
c0f33de0c8 Remove dead code 2024-03-13 21:56:09 +05:30
Vishnu Mohandas
72719d2234 [server] Add admin API to change email address (#1086)
## Tests
- [x] Tested on a local instance to verify that the email is updated as
expected
2024-03-13 17:37:14 +05:30
vishnukvmd
77276d8d6c [server] Add admin API to change email address 2024-03-13 17:31:38 +05:30
Vishnu Mohandas
c6c7b0ab32 Ignore linter on the f-droid branch (#1083) 2024-03-13 14:58:01 +05:30
vishnukvmd
df316463ef Ignore linter on the f-droid branch 2024-03-13 14:57:19 +05:30
Manav Rathi
be9af355ce [web] Fix the upload tests (#1082)
The current approach wasn't working. Not sure what caused it to stop
working,
but anyway that was an hacky import, as evidenced by the ungainly
warning
webpack would print on `yarn dev`. So instead of taking the path, we
just take
the JSON contents directly, sidestepping all that.

**Tested by**

Rerunning the upload tests
2024-03-13 14:31:20 +05:30
Manav Rathi
2faef37f4b Fix the upload tests
The current approach wasn't working. Not sure what caused it to stop working,
but anyway that was an hacky import, as evidenced by the ungainly warning
webpack would print on `yarn dev`. So instead of taking the path, we just take
the JSON contents directly, sidestepping all that.

**Tested by**

Rerunning the upload tests
2024-03-13 14:25:07 +05:30
vishnukvmd
417621b17c Pull code for transistor-background-fetch 2024-03-13 14:14:19 +05:30
vishnukvmd
8322540732 Add submodule for Flutter 2024-03-13 14:13:40 +05:30
vishnukvmd
2d61be37bb Add submodule for Isar 2024-03-13 14:12:23 +05:30
github-actions[bot]
83aa3db795 [mobile] New translations (#740)
New translations from
[Crowdin](https://crowdin.com/project/ente-photos-app)

Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2024-03-13 13:55:47 +05:30
Crowdin Bot
5db0da9aaf New Crowdin translations by GitHub Action 2024-03-13 13:55:20 +05:30
Vishnu Mohandas
3abc7249bd Cleanup code maintained specifically for F-Droid (#1081) 2024-03-13 13:54:58 +05:30
vishnukvmd
2a10aa7d61 Merge branch 'fdroid_cleanup' into f-droid 2024-03-13 13:52:25 +05:30
Ashil
6ef1da68e0 [photos][mobile] Performance improvement (#1080)
## Description

Stack has `clipBehaviour = Clip.hardEdge` by default. This is necessary
if content inside the stack is overflowing it's boundary and it has to
be clipped. Clipping is expensive so it's worth removing it when it
makes sense.

In this case of `GalleryFileWidget`, content doesn't overflow the
Stack's boundary so the clip operation can be removed by setting
`clipBehaviour = Clip.none`.
2024-03-13 13:52:03 +05:30
vishnukvmd
1c1c9bb0d7 Update docs 2024-03-13 13:51:20 +05:30
vishnukvmd
b96e7341e3 Remove thirdparty dependency on transistor-background-fetch 2024-03-13 13:47:33 +05:30
vishnukvmd
163c5de1cc Remove Isar as a submodule 2024-03-13 13:46:59 +05:30
vishnukvmd
124ef86054 Remove flutter as a submodule 2024-03-13 13:45:44 +05:30
vishnukvmd
004eb310b3 Prepare for F-Droid 2024-03-13 13:43:46 +05:30
Vishnu Mohandas
ccb6a4a283 v0.8.70 (#1079) 2024-03-13 12:43:51 +05:30
vishnukvmd
a3c80556d2 v0.8.70 2024-03-13 12:43:31 +05:30
Vishnu Mohandas
851ce5de73 Fix path to APK (#1078) 2024-03-13 12:23:16 +05:30
vishnukvmd
f8d956d47f Fix path to APK 2024-03-13 12:22:11 +05:30
Neeraj Gupta
7543dc6b57 [docs] Update custom server section for cli 2024-03-13 12:03:56 +05:30
44 changed files with 465 additions and 959 deletions

View File

@@ -3,7 +3,7 @@ name: "Lint (mobile)"
on:
# Run on every push to a branch other than main that changes mobile/
push:
branches-ignore: [main]
branches-ignore: [main, f-droid]
paths:
- "mobile/**"
- ".github/workflows/mobile-lint.yml"

View File

@@ -49,7 +49,7 @@ jobs:
SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }}
- name: Checksum
run: sha256sum build/app/outputs/flutter-apk/ente.apk > build/app/outputs/flutter-apk/sha256sum
run: sha256sum build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk > build/app/outputs/flutter-apk/sha256sum
- name: Create a draft GitHub release
uses: ncipollo/release-action@v1

13
.gitmodules vendored
View File

@@ -9,16 +9,9 @@
[submodule "auth/assets/simple-icons"]
path = auth/assets/simple-icons
url = https://github.com/simple-icons/simple-icons.git
[submodule "mobile/thirdparty/flutter"]
path = mobile/thirdparty/flutter
url = https://github.com/flutter/flutter.git
branch = stable
[submodule "mobile/plugins/clip_ggml"]
path = mobile/plugins/clip_ggml
url = https://github.com/ente-io/clip-ggml.git
[submodule "mobile/thirdparty/isar"]
path = mobile/thirdparty/isar
url = https://github.com/isar/isar
[submodule "web/apps/photos/thirdparty/ffmpeg-wasm"]
path = web/apps/photos/thirdparty/ffmpeg-wasm
url = https://github.com/abhinavkgrd/ffmpeg.wasm.git
@@ -27,3 +20,9 @@
path = web/apps/photos/thirdparty/photoswipe
url = https://github.com/ente-io/PhotoSwipe.git
branch = single-thread
[submodule "mobile/thirdparty/isar"]
path = mobile/thirdparty/isar
url = https://github.com/isar/isar
[submodule "mobile/thirdparty/flutter"]
path = mobile/thirdparty/flutter
url = https://github.com/flutter/flutter

View File

@@ -0,0 +1 @@
{}

View File

@@ -144,6 +144,7 @@
"enterCodeHint": "Geben Sie den 6-stelligen Code \naus Ihrer Authentifikator-App ein.",
"lostDeviceTitle": "Gerät verloren?",
"twoFactorAuthTitle": "Zwei-Faktor-Authentifizierung",
"passkeyAuthTitle": "Passkey Authentifizierung",
"recoverAccount": "Konto wiederherstellen",
"enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein",
"recover": "Wiederherstellen",
@@ -404,5 +405,15 @@
"signOutOtherDevices": "Andere Geräte abmelden",
"doNotSignOut": "Nicht abmelden",
"hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
"hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!"
"hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!",
"waitingForBrowserRequest": "Warten auf Browseranfrage...",
"launchPasskeyUrlAgain": "Passwort-URL erneut starten",
"passkey": "Passkey",
"developerSettingsWarning": "Sind Sie sicher, dass Sie die Entwicklereinstellungen ändern möchten?",
"developerSettings": "Entwicklereinstellungen",
"serverEndpoint": "Server Endpunkt",
"invalidEndpoint": "Ungültiger Endpunkt",
"invalidEndpointMessage": "Der eingegebene Endpunkt ist ungültig. Bitte geben Sie einen gültigen Endpunkt ein und versuchen Sie es erneut.",
"endpointUpdatedMessage": "Endpunkt erfolgreich aktualisiert",
"customEndpoint": "Mit {endpoint} verbunden"
}

View File

@@ -144,6 +144,7 @@
"enterCodeHint": "認証アプリに表示された 6 桁のコードを入力してください",
"lostDeviceTitle": "デバイスを紛失しましたか?",
"twoFactorAuthTitle": "2 要素認証",
"passkeyAuthTitle": "パスキー認証",
"recoverAccount": "アカウントを回復",
"enterRecoveryKeyHint": "回復キーを入力",
"recover": "回復",
@@ -404,5 +405,15 @@
"signOutOtherDevices": "他のデバイスからサインアウトする",
"doNotSignOut": "サインアウトしない",
"hearUsWhereTitle": "Ente についてどのようにお聞きになりましたか?(任意)",
"hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!"
"hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!",
"waitingForBrowserRequest": "ブラウザのリクエストを待っています...",
"launchPasskeyUrlAgain": "パスキーのURLを再度起動する",
"passkey": "パスキー",
"developerSettingsWarning": "開発者向け設定を変更してもよろしいですか?",
"developerSettings": "開発者向け設定",
"serverEndpoint": "サーバーエンドポイント",
"invalidEndpoint": "無効なエンドポイントです",
"invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。",
"endpointUpdatedMessage": "エンドポイントの更新に成功しました",
"customEndpoint": "{endpoint} に接続しました"
}

View File

@@ -144,6 +144,7 @@
"enterCodeHint": "Digite o código de 6 dígitos de\nseu aplicativo autenticador",
"lostDeviceTitle": "Perdeu seu dispositivo?",
"twoFactorAuthTitle": "Autenticação de dois fatores",
"passkeyAuthTitle": "Autenticação via Chave de acesso",
"recoverAccount": "Recuperar conta",
"enterRecoveryKeyHint": "Digite sua chave de recuperação",
"recover": "Recuperar",
@@ -404,5 +405,15 @@
"signOutOtherDevices": "Terminar sessão em outros dispositivos",
"doNotSignOut": "Não encerrar sessão",
"hearUsWhereTitle": "Como você ouviu sobre o Ente? (opcional)",
"hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!"
"hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!",
"waitingForBrowserRequest": "Aguardando solicitação do navegador...",
"launchPasskeyUrlAgain": "Iniciar a URL de chave de acesso novamente",
"passkey": "Chave de acesso",
"developerSettingsWarning": "Tem certeza de que deseja modificar as configurações de Desenvolvedor?",
"developerSettings": "Configurações de desenvolvedor",
"serverEndpoint": "Endpoint do servidor",
"invalidEndpoint": "Endpoint inválido",
"invalidEndpointMessage": "Desculpe, o endpoint que você inseriu é inválido. Por favor, insira um endpoint válido e tente novamente.",
"endpointUpdatedMessage": "Endpoint atualizado com sucesso",
"customEndpoint": "Conectado a {endpoint}"
}

View File

@@ -131,6 +131,16 @@
"about": "Om",
"terms": "Villkor",
"warning": "Varning",
"importSuccessDesc": "Du har importerat {count} koder!",
"@importSuccessDesc": {
"placeholders": {
"count": {
"description": "The number of codes imported",
"type": "int",
"example": "1"
}
}
},
"pendingSyncs": "Varning",
"activeSessions": "Aktiva sessioner",
"enterPassword": "Ange lösenord",
@@ -143,5 +153,7 @@
"iOSOkButton": "OK",
"@iOSOkButton": {
"description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters."
}
},
"noInternetConnection": "Ingen internetanslutning",
"pleaseCheckYourInternetConnectionAndTryAgain": "Kontrollera din internetanslutning och försök igen."
}

View File

@@ -408,5 +408,12 @@
"hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!",
"waitingForBrowserRequest": "正在等待浏览器请求...",
"launchPasskeyUrlAgain": "再次启动 通行密钥 URL",
"passkey": "通行密钥"
"passkey": "通行密钥",
"developerSettingsWarning": "您确定要修改开发者设置吗?",
"developerSettings": "开发者设置",
"serverEndpoint": "服务器端点",
"invalidEndpoint": "端点无效",
"invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。",
"endpointUpdatedMessage": "端点更新成功",
"customEndpoint": "已连接至 {endpoint}"
}

View File

@@ -45,8 +45,7 @@ You can alternatively install the build from PlayStore or F-Droid.
## 🧑‍💻 Building from source
1. [Install Flutter v3.13.4](https://flutter.dev/docs/get-started/install) or
set the Path of Flutter SDK to `thirdparty/flutter/bin`.
1. [Install Flutter v3.13.4](https://flutter.dev/docs/get-started/install).
2. Pull in all submodules with `git submodule update --init --recursive`

View File

@@ -18,10 +18,7 @@ allprojects {
google()
jcenter()
mavenCentral()
// mavenLocal() // for FDroid
maven {
url "${project(':background_fetch').projectDir}/libs"
}
mavenLocal() // for FDroid
}
}

View File

@@ -1,3 +0,0 @@
{
"dart.flutterSdkPath": "thirdparty/flutter/bin"
}

View File

@@ -1,6 +1,6 @@
ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos.
Se você está procurando uma alternativa ao Google Photos com foco em privacidade, veio ao lugar certo. Com ente, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
Se você está procurando uma alternativa ao Google Fotos com foco em privacidade, você veio ao lugar certo. Com ente, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
Temos aplicativos de código aberto em todas as plataformas, Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee).

View File

@@ -1,12 +1,12 @@
ente 是一个简单的应用程序来备份和分享您的照片和视频。
如果你一直在寻找一个隐私友好的Google Photos替代品那么你就来对地方了。 使用 Ente它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。
如果你一直在寻找一个隐私友好的Google Photos替代品那么你就来对地方了。 使用 Ente它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。 使用 Ente它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。
我们在Android、iOS、web 和桌面上有开源应用, 和您的照片将以端到端加密方式 (e2ee) 无缝同步。
ente也使分享相册给自己的爱人、亲人变得轻而易举即使他们可能并不使用ente。 您可以分享可公开查看的链接使他们可以查看您的相册并通过添加照片来协作而不需要注册账户或下载app。
ente也使分享相册给自己的爱人、亲人变得轻而易举即使他们可能并不使用ente。 您可以分享可公开查看的链接使他们可以查看您的相册并通过添加照片来协作而不需要注册账户或下载app。 权限
您的加密数据已复制到三个不同的地点,包括巴黎的一个安全屋。 我们认真对待子孙后代,并确保您的回忆比您长寿。
您的加密数据已复制到三个不同的地点,包括巴黎的一个安全屋。 我们认真对待子孙后代,并确保您的回忆比您长寿。 我们认真对待子孙后代,并确保您的回忆比您长寿。
我们来这里是为了打造有史以来最安全的照片应用,来和我们一起前行!
@@ -30,7 +30,7 @@ ente也使分享相册给自己的爱人、亲人变得轻而易举即使他
ente需要特定权限以执行作为图像存储提供商的职责相关内容可以在此链接查阅https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md
价格
我们不会提供永久免费计划,因为我们必须保持可持续性,经受住时间的考验。 相反,我们向您提供了价格实惠、可自由分享的订阅计划。 您可以在 ente.io 找到更多信息。
我们不会提供永久免费计划,因为我们必须保持可持续性,经受住时间的考验。 相反,我们向您提供了价格实惠、可自由分享的订阅计划。 您可以在 ente.io 找到更多信息。 相反,我们向您提供了价格实惠、可自由分享的订阅计划。 您可以在 ente.io 找到更多信息。
支持
我们对提供真人支持感到自豪。 如果您是我们的付费客户,您可以联系 team@ente.io 并在24小时内收到来自我们团队的回复。
我们对提供真人支持感到自豪。 我们对提供真人支持感到自豪。 如果您是我们的付费客户,您可以联系 team@ente.io 并在24小时内收到来自我们团队的回复。

View File

@@ -1,6 +1,6 @@
Ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos.
Se você esteve procurando uma alternativa amigável à privacidade para preservar suas memórias, você veio ao lugar certo. Com ente, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
Se você esteve procurando uma alternativa amigável à privacidade para preservar suas memórias, você veio ao lugar certo. Com Ente, elas são armazenadas com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-las.
Temos aplicativos de código aberto em Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee).

View File

@@ -1,6 +1,6 @@
Ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos.
Se você esteve procurando uma alternativa amigável à privacidade para preservar suas memórias, você veio ao lugar certo. Com ente, eles são armazenados com criptografados de ponta a ponta (e2ee). Isso significa que só você pode vê-los.
Se você esteve procurando uma alternativa amigável à privacidade para preservar suas memórias, você veio ao lugar certo. Com Ente, elas são armazenadas com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-las.
Temos aplicativos de código aberto em todas as plataformas, Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee).

View File

@@ -406,6 +406,15 @@
},
"photoGridSize": "Fotorastergröße",
"manageDeviceStorage": "Gerätespeicher verwalten",
"machineLearning": "Maschinelles Lernen",
"magicSearch": "Magische Suche",
"magicSearchDescription": "Bitte beachten Sie, dass dies mehr Bandbreite nutzt und zu einem höheren Akkuverbrauch führt, bis alle Elemente indiziert sind.",
"loadingModel": "Lade Modelle herunter...",
"waitingForWifi": "Warte auf WLAN...",
"status": "Status",
"indexedItems": "Indizierte Elemente",
"pendingItems": "Ausstehende Elemente",
"clearIndexes": "Indexe löschen",
"selectFoldersForBackup": "Ordner für Sicherung auswählen",
"selectedFoldersWillBeEncryptedAndBackedUp": "Ausgewählte Ordner werden verschlüsselt und gesichert",
"unselectAll": "Alle demarkieren",
@@ -1178,7 +1187,17 @@
"changeLocationOfSelectedItems": "Standort der gewählten Elemente ändern?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Änderungen des Standorts werden nur in ente sichtbar sein",
"cleanUncategorized": "Unkategorisiert leeren",
"joinDiscord": "Join Discord",
"locations": "Locations",
"descriptions": "Descriptions"
"cleanUncategorizedDescription": "Entferne alle Dateien von \"Unkategorisiert\" die in anderen Alben vorhanden sind",
"waitingForVerification": "Warte auf Bestätigung...",
"passkey": "Passkey",
"passkeyAuthTitle": "Passkey-Verifizierung",
"verifyPasskey": "Passkey verifizieren",
"playOnTv": "Album auf dem Fernseher wiedergeben",
"pair": "Koppeln",
"deviceNotFound": "Gerät nicht gefunden",
"castInstruction": "Besuche cast.ente.io auf dem Gerät, das du verbinden möchtest.\n\nGib den unten angegebenen Code ein, um das Album auf deinem Fernseher abzuspielen.",
"deviceCodeHint": "Code eingeben",
"joinDiscord": "Discord beitreten",
"locations": "Orte",
"descriptions": "Beschreibungen"
}

View File

@@ -1187,16 +1187,17 @@
"changeLocationOfSelectedItems": "Alterar o local dos itens selecionados?",
"editsToLocationWillOnlyBeSeenWithinEnte": "Edições para local só serão vistas dentro do Ente",
"cleanUncategorized": "Limpar Sem Categoria",
"waitingForBrowserRequest": "Aguardando solicitação do navegador...",
"launchPasskeyUrlAgain": "Iniciar a URL de chave de acesso novamente",
"cleanUncategorizedDescription": "Remover todos os arquivos de Não Categorizados que estão presentes em outros álbuns",
"waitingForVerification": "Esperando por verificação...",
"passkey": "Chave de acesso",
"passkeyAuthTitle": "Autenticação via Chave de acesso",
"verifyPasskey": "Verificar chave de acesso",
"playOnTv": "Reproduzir álbum na TV",
"pair": "Parear",
"deviceNotFound": "Dispositivo não encontrado",
"castInstruction": "Visite cast.ente.io no dispositivo que você deseja parear.\n\ndigite o código abaixo para reproduzir o álbum em sua TV.",
"deviceCodeHint": "Insira o código",
"joinDiscord": "Junte-se ao Discord",
"locations": "Locations",
"descriptions": "Descriptions"
"locations": "Locais",
"descriptions": "Descrições"
}

View File

@@ -1187,16 +1187,17 @@
"changeLocationOfSelectedItems": "确定要更改所选项目的位置吗?",
"editsToLocationWillOnlyBeSeenWithinEnte": "对位置的编辑只能在 Ente 内看到",
"cleanUncategorized": "清除未分类的",
"waitingForBrowserRequest": "正在等待浏览器请求...",
"launchPasskeyUrlAgain": "再次启动 通行密钥 URL",
"cleanUncategorizedDescription": "从“未分类”中删除其他相册中存在的所有文件",
"waitingForVerification": "等待验证...",
"passkey": "通行密钥",
"passkeyAuthTitle": "通行密钥认证",
"verifyPasskey": "验证通行密钥",
"playOnTv": "在电视上播放相册",
"pair": "配对",
"deviceNotFound": "未发现设备",
"castInstruction": "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。",
"deviceCodeHint": "输入代码",
"joinDiscord": "加入 Discord",
"locations": "Locations",
"descriptions": "Descriptions"
"locations": "位置",
"descriptions": "描述"
}

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import "package:adaptive_theme/adaptive_theme.dart";
import 'package:background_fetch/background_fetch.dart';
import "package:computer/computer.dart";
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import "package:flutter/rendering.dart";
@@ -34,7 +33,6 @@ import "package:photos/services/location_service.dart";
import "package:photos/services/machine_learning/machine_learning_controller.dart";
import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart';
import 'package:photos/services/memories_service.dart';
import 'package:photos/services/push_service.dart';
import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/services/search_service.dart';
import "package:photos/services/storage_bonus_service.dart";
@@ -216,14 +214,6 @@ Future<void> _init(bool isBackground, {String via = ''}) async {
unawaited(HomeWidgetService.instance.initHomeWidget());
}
if (Platform.isIOS) {
// ignore: unawaited_futures
PushService.instance.init().then((_) {
FirebaseMessaging.onBackgroundMessage(
_firebaseMessagingBackgroundHandler,
);
});
}
unawaited(FeatureFlagService.instance.init());
unawaited(SemanticSearchService.instance.init());
MachineLearningController.instance.init();
@@ -334,35 +324,6 @@ Future<void> _killBGTask([String? taskId]) async {
}
}
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
final bool isRunningInFG = await _isRunningInForeground(); // hb
final bool isInForeground = AppLifecycleService.instance.isForeground;
if (_isProcessRunning) {
_logger.info(
"Background push received when app is alive and runningInFS: $isRunningInFG inForeground: $isInForeground",
);
if (PushService.shouldSync(message)) {
await _sync('firebaseBgSyncActiveProcess');
}
} else {
// App is dead
// ignore: unawaited_futures
_runWithLogs(
() async {
_logger.info("Background push received");
if (Platform.isIOS) {
_scheduleSuicide(kBGPushTimeout); // To prevent OS from punishing us
}
await _init(true, via: 'firebasePush');
if (PushService.shouldSync(message)) {
await _sync('firebaseBgSyncNoActiveProcess');
}
},
prefix: "[fbg]",
);
}
}
Future<void> _logFGHeartBeatInfo() async {
final bool isRunningInFG = await _isRunningInForeground();
final prefs = await SharedPreferences.getInstance();

View File

@@ -4,7 +4,6 @@ import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
// import 'package:flutter/foundation.dart';
// import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/errors.dart';
import 'package:photos/core/network/network.dart';
@@ -35,6 +34,7 @@ class BillingService {
final _logger = Logger("BillingService");
final _enteDio = NetworkClient.instance.enteDio;
// ignore: unused_field
bool _isOnSubscriptionPage = false;
Future<BillingPlans>? _future;
@@ -44,23 +44,6 @@ class BillingService {
// await FlutterInappPurchase.instance.initConnection;
// FlutterInappPurchase.instance.clearTransactionIOS();
// }
InAppPurchase.instance.purchaseStream.listen((purchases) {
if (_isOnSubscriptionPage) {
return;
}
for (final purchase in purchases) {
if (purchase.status == PurchaseStatus.purchased) {
verifySubscription(
purchase.productID,
purchase.verificationData.serverVerificationData,
).then((response) {
InAppPurchase.instance.completePurchase(purchase);
});
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
InAppPurchase.instance.completePurchase(purchase);
}
}
});
}
void clearCache() {

View File

@@ -1,103 +0,0 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/core/network/network.dart';
import 'package:photos/events/signed_in_event.dart';
import 'package:photos/services/sync_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
class PushService {
static const kFCMPushToken = "fcm_push_token";
static const kLastFCMTokenUpdationTime = "fcm_push_token_updation_time";
static const kFCMTokenUpdationIntervalInMicroSeconds = 30 * microSecondsInDay;
static const kPushAction = "action";
static const kSync = "sync";
static final PushService instance = PushService._privateConstructor();
static final _logger = Logger("PushService");
late SharedPreferences _prefs;
PushService._privateConstructor();
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
await Firebase.initializeApp();
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
_logger.info("Got a message whilst in the foreground!");
_handleForegroundPushMessage(message);
});
try {
if (Configuration.instance.hasConfiguredAccount()) {
await _configurePushToken();
} else {
Bus.instance.on<SignedInEvent>().listen((_) async {
// ignore: unawaited_futures
_configurePushToken();
});
}
} catch (e, s) {
_logger.severe("Could not configure push token", e, s);
}
}
Future<void> _configurePushToken() async {
final String? fcmToken = await FirebaseMessaging.instance.getToken();
final shouldForceRefreshServerToken =
DateTime.now().microsecondsSinceEpoch -
(_prefs.getInt(kLastFCMTokenUpdationTime) ?? 0) >
kFCMTokenUpdationIntervalInMicroSeconds;
if (fcmToken != null &&
(_prefs.getString(kFCMPushToken) != fcmToken ||
shouldForceRefreshServerToken)) {
final String? apnsToken = await FirebaseMessaging.instance.getAPNSToken();
try {
_logger.info("Updating token on server");
await _setPushTokenOnServer(fcmToken, apnsToken);
await _prefs.setString(kFCMPushToken, fcmToken);
await _prefs.setInt(
kLastFCMTokenUpdationTime,
DateTime.now().microsecondsSinceEpoch,
);
_logger.info("Push token updated on server");
} catch (e) {
_logger.severe("Could not set push token", e, StackTrace.current);
}
} else {
_logger.info("Skipping token update");
}
}
Future<void> _setPushTokenOnServer(
String fcmToken,
String? apnsToken,
) async {
await NetworkClient.instance.enteDio.post(
"/push/token",
data: {
"fcmToken": fcmToken,
"apnsToken": apnsToken,
},
);
}
void _handleForegroundPushMessage(RemoteMessage message) {
_logger.info("Message data: ${message.data}");
if (message.notification != null) {
_logger.info(
"Message also contained a notification: ${message.notification}",
);
}
if (shouldSync(message)) {
SyncService.instance.sync();
}
}
static bool shouldSync(RemoteMessage message) {
return message.data.containsKey(kPushAction) &&
message.data[kPushAction] == kSync;
}
}

View File

@@ -1,603 +0,0 @@
import 'dart:async';
import 'dart:io';
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/errors.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/subscription_purchased_event.dart';
import "package:photos/generated/l10n.dart";
import 'package:photos/models/billing_plan.dart';
import 'package:photos/models/subscription.dart';
import 'package:photos/models/user_details.dart';
import 'package:photos/services/billing_service.dart';
import "package:photos/services/update_service.dart";
import 'package:photos/services/user_service.dart';
import "package:photos/theme/colors.dart";
import "package:photos/theme/ente_theme.dart";
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/common/progress_dialog.dart';
import "package:photos/ui/components/captioned_text_widget.dart";
import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart";
import 'package:photos/ui/payment/child_subscription_widget.dart';
import 'package:photos/ui/payment/skip_subscription_widget.dart';
import 'package:photos/ui/payment/subscription_common_widgets.dart';
import 'package:photos/ui/payment/subscription_plan_widget.dart';
import "package:photos/ui/payment/view_add_on_widget.dart";
import "package:photos/utils/data_util.dart";
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:url_launcher/url_launcher_string.dart';
class StoreSubscriptionPage extends StatefulWidget {
final bool isOnboarding;
const StoreSubscriptionPage({
this.isOnboarding = false,
Key? key,
}) : super(key: key);
@override
State<StoreSubscriptionPage> createState() => _StoreSubscriptionPageState();
}
class _StoreSubscriptionPageState extends State<StoreSubscriptionPage> {
final _logger = Logger("SubscriptionPage");
final _billingService = BillingService.instance;
final _userService = UserService.instance;
Subscription? _currentSubscription;
late StreamSubscription _purchaseUpdateSubscription;
late ProgressDialog _dialog;
late UserDetails _userDetails;
late bool _hasActiveSubscription;
bool _hideCurrentPlanSelection = false;
late FreePlan _freePlan;
late List<BillingPlan> _plans;
bool _hasLoadedData = false;
bool _isLoading = false;
late bool _isActiveStripeSubscriber;
EnteColorScheme colorScheme = darkScheme;
// hasYearlyPlans is used to check if there are yearly plans for given store
bool hasYearlyPlans = false;
// _showYearlyPlan is used to determine if we should show the yearly plans
bool showYearlyPlan = false;
@override
void initState() {
_billingService.setIsOnSubscriptionPage(true);
_setupPurchaseUpdateStreamListener();
super.initState();
}
void _setupPurchaseUpdateStreamListener() {
_purchaseUpdateSubscription =
InAppPurchase.instance.purchaseStream.listen((purchases) async {
if (!_dialog.isShowing()) {
await _dialog.show();
}
for (final purchase in purchases) {
_logger.info("Purchase status " + purchase.status.toString());
if (purchase.status == PurchaseStatus.purchased) {
try {
final newSubscription = await _billingService.verifySubscription(
purchase.productID,
purchase.verificationData.serverVerificationData,
);
await InAppPurchase.instance.completePurchase(purchase);
String text = S.of(context).thankYouForSubscribing;
if (!widget.isOnboarding) {
final isUpgrade = _hasActiveSubscription &&
newSubscription.storage > _currentSubscription!.storage;
final isDowngrade = _hasActiveSubscription &&
newSubscription.storage < _currentSubscription!.storage;
if (isUpgrade) {
text = S.of(context).yourPlanWasSuccessfullyUpgraded;
} else if (isDowngrade) {
text = S.of(context).yourPlanWasSuccessfullyDowngraded;
}
}
showShortToast(context, text);
_currentSubscription = newSubscription;
_hasActiveSubscription = _currentSubscription!.isValid();
setState(() {});
await _dialog.hide();
Bus.instance.fire(SubscriptionPurchasedEvent());
if (widget.isOnboarding) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
} on SubscriptionAlreadyClaimedError catch (e) {
_logger.warning("subscription is already claimed ", e);
await _dialog.hide();
final String title = Platform.isAndroid
? S.of(context).playstoreSubscription
: S.of(context).appstoreSubscription;
final String id = Platform.isAndroid
? S.of(context).googlePlayId
: S.of(context).appleId;
final String message = S.of(context).subAlreadyLinkedErrMessage(id);
// ignore: unawaited_futures
showErrorDialog(context, title, message);
return;
} catch (e) {
_logger.warning("Could not complete payment ", e);
await _dialog.hide();
// ignore: unawaited_futures
showErrorDialog(
context,
S.of(context).paymentFailed,
S.of(context).paymentFailedTalkToProvider(
Platform.isAndroid ? "PlayStore" : "AppStore",
),
);
return;
}
} else if (Platform.isIOS && purchase.pendingCompletePurchase) {
await InAppPurchase.instance.completePurchase(purchase);
await _dialog.hide();
} else if (purchase.status == PurchaseStatus.error) {
await _dialog.hide();
}
}
});
}
@override
void dispose() {
_purchaseUpdateSubscription.cancel();
_billingService.setIsOnSubscriptionPage(false);
super.dispose();
}
@override
Widget build(BuildContext context) {
colorScheme = getEnteColorScheme(context);
if (!_isLoading) {
_isLoading = true;
_fetchSubData();
}
_dialog = createProgressDialog(context, S.of(context).pleaseWait);
final appBar = AppBar(
title: widget.isOnboarding
? null
: Text("${S.of(context).subscription}${kDebugMode ? ' Store' : ''}"),
);
return Scaffold(
appBar: appBar,
body: _getBody(),
);
}
bool _isFreePlanUser() {
return _currentSubscription != null &&
freeProductID == _currentSubscription!.productID;
}
Future<void> _fetchSubData() async {
// ignore: unawaited_futures
_userService.getUserDetailsV2(memoryCount: false).then((userDetails) async {
_userDetails = userDetails;
_currentSubscription = userDetails.subscription;
_hasActiveSubscription = _currentSubscription!.isValid();
_hideCurrentPlanSelection =
_currentSubscription?.attributes?.isCancelled ?? false;
showYearlyPlan = _currentSubscription!.isYearlyPlan();
final billingPlans = await _billingService.getBillingPlans();
_isActiveStripeSubscriber =
_currentSubscription!.paymentProvider == stripe &&
_currentSubscription!.isValid();
_plans = billingPlans.plans.where((plan) {
final productID = _isActiveStripeSubscriber
? plan.stripeID
: Platform.isAndroid
? plan.androidID
: plan.iosID;
return productID.isNotEmpty;
}).toList();
hasYearlyPlans = _plans.any((plan) => plan.period == 'year');
if (showYearlyPlan && hasYearlyPlans) {
_plans = _plans.where((plan) => plan.period == 'year').toList();
} else {
_plans = _plans.where((plan) => plan.period != 'year').toList();
}
_freePlan = billingPlans.freePlan;
_hasLoadedData = true;
if (mounted) {
setState(() {});
}
});
}
Widget _getBody() {
if (_hasLoadedData) {
if (_userDetails.isPartOfFamily() && !_userDetails.isFamilyAdmin()) {
return ChildSubscriptionWidget(userDetails: _userDetails);
} else {
return _buildPlans();
}
}
return const EnteLoadingWidget();
}
Widget _buildPlans() {
final widgets = <Widget>[];
widgets.add(
SubscriptionHeaderWidget(
isOnboarding: widget.isOnboarding,
currentUsage: _userDetails.getFamilyOrPersonalUsage(),
),
);
widgets.addAll([
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: _isActiveStripeSubscriber
? _getStripePlanWidgets()
: _getMobilePlanWidgets(),
),
const Padding(padding: EdgeInsets.all(8)),
]);
if (hasYearlyPlans) {
widgets.add(_showSubscriptionToggle());
}
if (_currentSubscription != null) {
widgets.add(
ValidityWidget(
currentSubscription: _currentSubscription,
bonusData: _userDetails.bonusData,
),
);
}
if (_currentSubscription!.productID == freeProductID) {
if (widget.isOnboarding) {
widgets.add(SkipSubscriptionWidget(freePlan: _freePlan));
}
widgets.add(
SubFaqWidget(isOnboarding: widget.isOnboarding),
);
}
if (_hasActiveSubscription &&
_currentSubscription!.productID != freeProductID) {
if (_isActiveStripeSubscriber) {
widgets.add(
Padding(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
child: Text(
S.of(context).visitWebToManage,
style: getEnteTextTheme(context).small.copyWith(
color: colorScheme.textMuted,
),
),
),
);
} else {
widgets.add(
Padding(
padding: const EdgeInsets.fromLTRB(16, 40, 16, 4),
child: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: S.of(context).paymentDetails,
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 4,
alignCaptionedTextToLeft: true,
onTap: () async {
_onPlatformRestrictedPaymentDetailsClick();
},
),
),
);
}
}
if (!widget.isOnboarding) {
widgets.add(
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
child: MenuItemWidget(
captionedTextWidget: CaptionedTextWidget(
title: _isFreePlanUser()
? S.of(context).familyPlans
: S.of(context).manageFamily,
),
menuItemColor: colorScheme.fillFaint,
trailingWidget: Icon(
Icons.chevron_right_outlined,
color: colorScheme.strokeBase,
),
singleBorderRadius: 4,
alignCaptionedTextToLeft: true,
onTap: () async {
unawaited(
_billingService.launchFamilyPortal(context, _userDetails),
);
},
),
),
);
widgets.add(ViewAddOnButton(_userDetails.bonusData));
widgets.add(const SizedBox(height: 80));
}
return SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widgets,
),
);
}
void _onPlatformRestrictedPaymentDetailsClick() {
final String paymentProvider = _currentSubscription!.paymentProvider;
if (paymentProvider == appStore && !Platform.isAndroid) {
launchUrlString("https://apps.apple.com/account/billing");
} else if (paymentProvider == playStore && Platform.isAndroid) {
launchUrlString(
"https://play.google.com/store/account/subscriptions?sku=" +
_currentSubscription!.productID +
"&package=io.ente.photos",
);
} else if (paymentProvider == stripe) {
showErrorDialog(
context,
S.of(context).sorry,
S.of(context).visitWebToManage,
);
} else {
final String capitalizedWord = paymentProvider.isNotEmpty
? '${paymentProvider[0].toUpperCase()}${paymentProvider.substring(1).toLowerCase()}'
: '';
showErrorDialog(
context,
S.of(context).sorry,
S.of(context).contactToManageSubscription(capitalizedWord),
);
}
}
Future<void> _filterStorePlansForUi() async {
final billingPlans = await _billingService.getBillingPlans();
_plans = billingPlans.plans.where((plan) {
final productID = _isActiveStripeSubscriber
? plan.stripeID
: Platform.isAndroid
? plan.androidID
: plan.iosID;
return productID.isNotEmpty;
}).toList();
hasYearlyPlans = _plans.any((plan) => plan.period == 'year');
if (showYearlyPlan) {
_plans = _plans.where((plan) => plan.period == 'year').toList();
} else {
_plans = _plans.where((plan) => plan.period != 'year').toList();
}
setState(() {});
}
Widget _showSubscriptionToggle() {
Widget planText(String title, bool reduceOpacity) {
return Padding(
padding: const EdgeInsets.only(left: 4, right: 4),
child: Text(
title,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurface
.withOpacity(reduceOpacity ? 0.5 : 1.0),
),
),
);
}
return Container(
padding: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 2),
margin: const EdgeInsets.only(bottom: 6),
child: Column(
children: [
RepaintBoundary(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
planText(S.of(context).monthly, showYearlyPlan),
Switch(
value: showYearlyPlan,
activeColor: Colors.white,
inactiveThumbColor: Colors.white,
activeTrackColor: getEnteColorScheme(context).strokeMuted,
onChanged: (value) async {
showYearlyPlan = value;
await _filterStorePlansForUi();
},
),
planText(S.of(context).yearly, !showYearlyPlan),
],
),
),
_isFreePlanUser() && !UpdateService.instance.isPlayStoreFlavor()
? Text(
S.of(context).twoMonthsFreeOnYearlyPlans,
style: getEnteTextTheme(context).miniMuted,
)
: const SizedBox.shrink(),
const Padding(padding: EdgeInsets.all(8)),
],
),
);
}
List<Widget> _getStripePlanWidgets() {
final List<Widget> planWidgets = [];
bool foundActivePlan = false;
for (final plan in _plans) {
final productID = plan.stripeID;
if (productID.isEmpty) {
continue;
}
final isActive = _hasActiveSubscription &&
_currentSubscription!.productID == productID;
if (isActive) {
foundActivePlan = true;
}
planWidgets.add(
Material(
color: Colors.transparent,
child: InkWell(
onTap: () async {
if (isActive) {
return;
}
// ignore: unawaited_futures
showErrorDialog(
context,
S.of(context).sorry,
S.of(context).visitWebToManage,
);
},
child: SubscriptionPlanWidget(
storage: plan.storage,
price: plan.price,
period: plan.period,
isActive: isActive && !_hideCurrentPlanSelection,
),
),
),
);
}
if (!foundActivePlan && _hasActiveSubscription) {
_addCurrentPlanWidget(planWidgets);
}
return planWidgets;
}
List<Widget> _getMobilePlanWidgets() {
bool foundActivePlan = false;
final List<Widget> planWidgets = [];
if (_hasActiveSubscription &&
_currentSubscription!.productID == freeProductID) {
foundActivePlan = true;
planWidgets.add(
SubscriptionPlanWidget(
storage: _freePlan.storage,
price: S.of(context).freeTrial,
period: "",
isActive: true,
),
);
}
for (final plan in _plans) {
final productID = Platform.isAndroid ? plan.androidID : plan.iosID;
final isActive = _hasActiveSubscription &&
_currentSubscription!.productID == productID;
if (isActive) {
foundActivePlan = true;
}
planWidgets.add(
Material(
child: InkWell(
onTap: () async {
if (isActive) {
return;
}
final int addOnBonus =
_userDetails.bonusData?.totalAddOnBonus() ?? 0;
if (_userDetails.getFamilyOrPersonalUsage() >
(plan.storage + addOnBonus)) {
_logger.warning(
" familyUsage ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage())}"
" plan storage ${convertBytesToReadableFormat(plan.storage)} "
"addOnBonus ${convertBytesToReadableFormat(addOnBonus)},"
"overshooting by ${convertBytesToReadableFormat(_userDetails.getFamilyOrPersonalUsage() - (plan.storage + addOnBonus))}",
);
// ignore: unawaited_futures
showErrorDialog(
context,
S.of(context).sorry,
S.of(context).youCannotDowngradeToThisPlan,
);
return;
}
await _dialog.show();
final ProductDetailsResponse response =
await InAppPurchase.instance.queryProductDetails({productID});
if (response.notFoundIDs.isNotEmpty) {
final errMsg = "Could not find products: " +
response.notFoundIDs.toString();
_logger.severe(errMsg);
await _dialog.hide();
await showGenericErrorDialog(
context: context,
error: Exception(errMsg),
);
return;
}
final isCrossGradingOnAndroid = Platform.isAndroid &&
_hasActiveSubscription &&
_currentSubscription!.productID != freeProductID &&
_currentSubscription!.productID != plan.androidID;
if (isCrossGradingOnAndroid) {
await _dialog.hide();
// ignore: unawaited_futures
showErrorDialog(
context,
S.of(context).couldNotUpdateSubscription,
S.of(context).pleaseContactSupportAndWeWillBeHappyToHelp,
);
return;
} else {
await InAppPurchase.instance.buyNonConsumable(
purchaseParam: PurchaseParam(
productDetails: response.productDetails[0],
),
);
}
},
child: SubscriptionPlanWidget(
storage: plan.storage,
price: plan.price,
period: plan.period,
isActive: isActive,
),
),
),
);
}
if (!foundActivePlan && _hasActiveSubscription) {
_addCurrentPlanWidget(planWidgets);
}
return planWidgets;
}
void _addCurrentPlanWidget(List<Widget> planWidgets) {
int activePlanIndex = 0;
for (; activePlanIndex < _plans.length; activePlanIndex++) {
if (_plans[activePlanIndex].storage > _currentSubscription!.storage) {
break;
}
}
planWidgets.insert(
activePlanIndex,
Material(
child: InkWell(
onTap: () {},
child: SubscriptionPlanWidget(
storage: _currentSubscription!.storage,
price: _currentSubscription!.price,
period: _currentSubscription!.period,
isActive: true,
),
),
),
);
}
}

View File

@@ -1,26 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/services/feature_flag_service.dart';
import 'package:photos/services/update_service.dart';
import "package:photos/ui/payment/store_subscription_page.dart";
import 'package:photos/ui/payment/stripe_subscription_page.dart';
StatefulWidget getSubscriptionPage({bool isOnBoarding = false}) {
if (UpdateService.instance.isIndependentFlavor()) {
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
}
if (FeatureFlagService.instance.enableStripe() &&
_isUserCreatedPostStripeSupport()) {
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
} else {
return StoreSubscriptionPage(isOnboarding: isOnBoarding);
}
}
// return true if the user was created after we added support for stripe payment
// on frame. We do this check to avoid showing Stripe payment option for earlier
// users who might have paid via playStore. This method should be removed once
// we have better handling for active play/app store subscription & stripe plans.
bool _isUserCreatedPostStripeSupport() {
return Configuration.instance.getUserID()! > 1580559962386460;
return StripeSubscriptionPage(isOnboarding: isOnBoarding);
}

View File

@@ -68,6 +68,7 @@ class GalleryFileWidget extends StatelessWidget {
: _onLongPressNoSelectionLimit(context, file);
},
child: Stack(
clipBehavior: Clip.none,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(1),

View File

@@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.8.68+588
version: 0.8.71+591
publish_to: none
environment:
@@ -64,8 +64,6 @@ dependencies:
file_saver:
# Use forked version till this PR is merged: https://github.com/incrediblezayed/file_saver/pull/87
git: https://github.com/jesims/file_saver.git
firebase_core: ^2.13.1
firebase_messaging: ^14.6.2
fk_user_agent: ^2.0.1
flutter:
sdk: flutter
@@ -99,7 +97,6 @@ dependencies:
http: ^1.1.0
image: ^4.0.17
image_editor: ^1.3.0
in_app_purchase: ^3.0.7
intl: ^0.18.0
isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0+1
@@ -207,7 +204,7 @@ flutter_icons:
android: "launcher_icon"
adaptive_icon_foreground: "assets/launcher_icon/ente-icon-foreground.png"
adaptive_icon_background: "#ffffff"
ios: true
ios: false # F-Droid
image_path: "assets/icon-light.png"
flutter_native_splash:

View File

@@ -1,7 +1,3 @@
# TODO: add `rustup@1.25.2` to `srclibs`
# TODO: verify if `gcc-multilib` or `libc-dev` is needed
$$rustup$$/rustup-init.sh -y
source $HOME/.cargo/env
cd thirdparty/isar/
bash tool/build_android.sh x86
bash tool/build_android.sh x64
@@ -15,3 +11,4 @@ mv libisar_android_x64.so libisar.so
mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86_64/
mv libisar_android_x86.so libisar.so
mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86/
cd ../../

View File

@@ -1,63 +0,0 @@
# Eclipse
.metadata
# Xcode
#
.DS_Store
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
#
#Pods/
# Eclipse
# built application files
*.apk
*.ap_
# files for the dex VM
*.dex
# Java class files
*.class
# generated files
bin/
gen/
# Local configuration file (sdk path, etc)
local.properties
# Eclipse project files
.classpath
.project
# Proguard folder generated by Eclipse
proguard/
# Intellij project files
*.iml
*.ipr
*.iws
.idea/

View File

@@ -1,11 +1,11 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
compileSdkVersion 30
defaultConfig {
applicationId "com.transistorsoft.backgroundfetch"
minSdkVersion 16
targetSdkVersion 26
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@@ -24,6 +24,5 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:26.1.0'
}

View File

@@ -3,11 +3,11 @@
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
classpath 'com.android.tools.build:gradle:4.1.3'
// NOTE: Do not place your application dependencies here; they belong
@@ -18,7 +18,7 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}
@@ -27,8 +27,8 @@ task clean(type: Delete) {
}
ext {
compileSdkVersion = 29
targetSdkVersion = 29
compileSdkVersion = 32
targetSdkVersion = 31
buildToolsVersion = "29.0.6"
appCompatVersion = "1.1.0"
appCompatVersion = "1.4.1"
}

View File

@@ -16,8 +16,8 @@ org.gradle.jvmargs=-Xmx1536m
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
VERSION_NAME=0.5.0
VERSION_CODE=15
VERSION_NAME=0.5.6
VERSION_CODE=21
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -1,7 +1,6 @@
#Thu Feb 09 18:40:48 IST 2023
#Thu Jul 15 09:21:17 EDT 2021
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
distributionSha256Sum=10065868c78f1207afb3a92176f99a37d753a513dff453abb6b5cceda4058cda
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip

View File

@@ -32,6 +32,10 @@ android {
mavenLocal()
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
@@ -40,15 +44,14 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "androidx.lifecycle:lifecycle-runtime:2.5.1"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
//implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion"
}
// Build Release
task buildRelease { task ->
task.dependsOn 'cordovaRelease'
task.dependsOn 'reactNativeRelease'
task.dependsOn 'nativeScriptRelease'
task.dependsOn 'flutterRelease'
}
@@ -59,7 +62,7 @@ task publishRelease { task ->
tasks["publishRelease"].mustRunAfter("assembleRelease")
tasks["publishRelease"].finalizedBy("publish")
def WORKSPACE_PATH = "/Volumes/Glyph2TB/Users/chris/workspace"
def WORKSPACE_PATH = "/Users/chris/workspace"
// Build local maven repo.
def LIBRARY_PATH = "com/transistorsoft/tsbackgroundfetch"
@@ -78,7 +81,7 @@ task buildLocalRepository { task ->
}
}
def cordovaDir = "$WORKSPACE_PATH/cordova/background-geolocation/cordova-plugin-background-fetch"
def cordovaDir = "$WORKSPACE_PATH/background-geolocation/cordova/cordova-plugin-background-fetch"
task cordovaRelease { task ->
task.dependsOn 'buildLocalRepository'
doLast {
@@ -95,7 +98,7 @@ task cordovaRelease { task ->
}
}
def reactNativeDir = "$WORKSPACE_PATH/react/background-geolocation/react-native-background-fetch"
def reactNativeDir = "$WORKSPACE_PATH/background-geolocation/react/react-native-background-fetch"
task reactNativeRelease { task ->
task.dependsOn 'buildLocalRepository'
doLast {
@@ -129,6 +132,19 @@ task flutterRelease { task ->
}
}
def capacitorDir = "$WORKSPACE_PATH/background-geolocation/capacitor/capacitor-background-fetch"
task capacitorRelease { task ->
task.dependsOn 'buildLocalRepository'
doLast {
delete "$capacitorDir/android/libs"
copy {
// Maven repo format.
from("$buildDir/repo-local")
into("$capacitorDir/android/libs")
}
}
}
task nativeScriptRelease(type: Copy) {
from('./build/outputs/aar/tsbackgroundfetch-release.aar')
into("$WORKSPACE_PATH/NativeScript/background-geolocation/nativescript-background-fetch/src/platforms/android/libs")

View File

@@ -3,11 +3,12 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.GET_TASKS" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application>
<receiver android:name="com.transistorsoft.tsbackgroundfetch.FetchAlarmReceiver" />
<service android:name="com.transistorsoft.tsbackgroundfetch.FetchJobService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="true" />
<receiver android:name="com.transistorsoft.tsbackgroundfetch.BootReceiver">
<receiver android:name="com.transistorsoft.tsbackgroundfetch.BootReceiver" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />

View File

@@ -110,6 +110,16 @@ public class BGTask {
removeTask(mTaskId);
}
static void reschedule(Context context, BackgroundFetchConfig existing, BackgroundFetchConfig config) {
BGTask existingTask = BGTask.getTask(existing.getTaskId());
if (existingTask != null) {
existingTask.finish();
}
cancel(context, existing.getTaskId(), existing.getJobId());
schedule(context, config);
}
static void schedule(Context context, BackgroundFetchConfig config) {
Log.d(BackgroundFetch.TAG, config.toString());
@@ -136,6 +146,8 @@ public class BGTask {
}
PersistableBundle extras = new PersistableBundle();
extras.putString(BackgroundFetchConfig.FIELD_TASK_ID, config.getTaskId());
extras.putLong("scheduled_at", System.currentTimeMillis());
builder.setExtras(extras);
if (android.os.Build.VERSION.SDK_INT >= 26) {
@@ -172,7 +184,7 @@ public class BGTask {
BackgroundFetch adapter = BackgroundFetch.getInstance(context);
if (adapter.isMainActivityActive()) {
if (!LifecycleManager.getInstance().isHeadless()) {
BackgroundFetch.Callback callback = adapter.getFetchCallback();
if (callback != null) {
callback.onTimeout(mTaskId);
@@ -246,7 +258,7 @@ public class BGTask {
static PendingIntent getAlarmPI(Context context, String taskId) {
Intent intent = new Intent(context, FetchAlarmReceiver.class);
intent.setAction(taskId);
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT|PendingIntent.FLAG_IMMUTABLE);
}
public String toString() {

View File

@@ -4,6 +4,8 @@ import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
@@ -76,6 +78,8 @@ public class BackgroundFetch {
private BackgroundFetch(Context context) {
mContext = context;
// Start Lifecycle Observer to be notified when app enters background.
getUiHandler().post(LifecycleManager.getInstance());
}
@SuppressWarnings({"unused"})
@@ -84,7 +88,16 @@ public class BackgroundFetch {
mFetchCallback = callback;
synchronized (mConfig) {
mConfig.put(config.getTaskId(), config);
if (mConfig.containsKey(config.getTaskId())) {
// Developer called `.configure` again. Re-configure the plugin by re-scheduling the fetch task.
BackgroundFetchConfig existing = mConfig.get(config.getTaskId());
Log.d(TAG, "Re-configured existing task");
BGTask.reschedule(mContext, existing, config);
mConfig.put(config.getTaskId(), config);
return;
} else {
mConfig.put(config.getTaskId(), config);
}
}
start(config.getTaskId());
}
@@ -224,8 +237,6 @@ public class BackgroundFetch {
}
private void registerTask(String taskId) {
Log.d(TAG, "- registerTask: " + taskId);
BackgroundFetchConfig config = getConfig(taskId);
if (config == null) {
@@ -234,6 +245,12 @@ public class BackgroundFetch {
}
config.save(mContext);
String msg = "- registerTask: " + taskId;
if (!config.getForceAlarmManager()) {
msg += " (jobId: " + config.getJobId() + ")";
}
Log.d(TAG, msg);
BGTask.schedule(mContext, config);
}
@@ -245,7 +262,7 @@ public class BackgroundFetch {
return;
}
if (isMainActivityActive()) {
if (!LifecycleManager.getInstance().isHeadless()) {
if (mFetchCallback != null) {
mFetchCallback.onFetch(task.getTaskId());
}
@@ -267,29 +284,6 @@ public class BackgroundFetch {
}
}
@SuppressWarnings({"WeakerAccess", "deprecation"})
public Boolean isMainActivityActive() {
Boolean isActive = false;
if (mContext == null || mFetchCallback == null) {
return false;
}
ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
try {
List<ActivityManager.RunningTaskInfo> tasks = activityManager.getRunningTasks(Integer.MAX_VALUE);
for (ActivityManager.RunningTaskInfo task : tasks) {
if (mContext.getPackageName().equalsIgnoreCase(task.baseActivity.getPackageName())) {
isActive = true;
break;
}
}
} catch (java.lang.SecurityException e) {
Log.w(TAG, "TSBackgroundFetch attempted to determine if MainActivity is active but was stopped due to a missing permission. Please add the permission 'android.permission.GET_TASKS' to your AndroidManifest. See Installation steps for more information");
throw e;
}
return isActive;
}
BackgroundFetchConfig getConfig(String taskId) {
synchronized (mConfig) {
return (mConfig.containsKey(taskId)) ? mConfig.get(taskId) : null;

View File

@@ -14,6 +14,15 @@ public class FetchJobService extends JobService {
@Override
public boolean onStartJob(final JobParameters params) {
PersistableBundle extras = params.getExtras();
long scheduleAt = extras.getLong("scheduled_at");
long dt = System.currentTimeMillis() - scheduleAt;
// Scheduled < 1s ago? Ignore.
if (dt < 1000) {
// JobScheduler always immediately fires an initial event on Periodic jobs -- We IGNORE these.
jobFinished(params, false);
return true;
}
final String taskId = extras.getString(BackgroundFetchConfig.FIELD_TASK_ID);
CompletionHandler completionHandler = new CompletionHandler() {

View File

@@ -0,0 +1,225 @@
package com.transistorsoft.tsbackgroundfetch;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Component for managing app life-cycle changes, including headless-mode.
*/
public class LifecycleManager implements DefaultLifecycleObserver, Runnable {
private static LifecycleManager sInstance;
public static LifecycleManager getInstance() {
if (sInstance == null) {
sInstance = getInstanceSynchronized();
}
return sInstance;
}
private static synchronized LifecycleManager getInstanceSynchronized() {
if (sInstance == null) sInstance = new LifecycleManager();
return sInstance;
}
private final List<OnHeadlessChangeCallback> mHeadlessChangeCallbacks = new ArrayList<>();
private final List<OnStateChangeCallback> mStateChangeCallbacks = new ArrayList<>();
private final Handler mHandler;
private Runnable mHeadlessChangeEvent;
private final AtomicBoolean mIsBackground = new AtomicBoolean(true);
private final AtomicBoolean mIsHeadless = new AtomicBoolean(true);
private final AtomicBoolean mStarted = new AtomicBoolean(false);
private final AtomicBoolean mPaused = new AtomicBoolean(false);
private LifecycleManager() {
mHandler = new Handler(Looper.getMainLooper());
onHeadlessChange(isHeadless -> {
if (isHeadless) {
Log.d(BackgroundFetch.TAG, "☯️ HeadlessMode? " + isHeadless);
}
});
}
/**
* Temporarily disable responding to pause/resume events. This was placed here for handling TSLocationManagerActivity events
* whose presentation causes onPause / onResume events that we don't want to react to.
*/
public void pause() {
mPaused.set(true);
}
/**
* Re-engage responding to pause/resume events.
*/
public void resume() {
mPaused.set(false);
}
/**
* Are we in the background?
* @return boolean
*/
public boolean isBackground() {
return mIsBackground.get();
}
/**
* Are we headless
* @return boolean
*/
public boolean isHeadless() {
return mIsHeadless.get();
}
/**
* Explicitly state that we are headless. Probably called when MainActivity is known to have been destroyed.
* @param value boolean
*/
public void setHeadless(boolean value) {
mIsHeadless.set(value);
if (mIsHeadless.get()) {
Log.d(BackgroundFetch.TAG,"☯️ HeadlessMode? " + mIsHeadless);
}
if (mHeadlessChangeEvent != null) {
mHandler.removeCallbacks(mHeadlessChangeEvent);
mStarted.set(true);
fireHeadlessChangeListeners();
}
}
/**
* Register Headless-mode change listener.
*/
public void onHeadlessChange(OnHeadlessChangeCallback callback) {
if (mStarted.get()) {
callback.onChange(mIsHeadless.get());
return;
}
synchronized (mHeadlessChangeCallbacks) {
mHeadlessChangeCallbacks.add(callback);
}
}
/**
* Register pause/resume listener.
*/
public void onStateChange(OnStateChangeCallback callback) {
synchronized (mStateChangeCallbacks) {
mStateChangeCallbacks.add(callback);
}
}
/**
* Regiser the LifecycleObserver
*/
@Override
public void run() {
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
}
@Override
public void onCreate(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG,"☯️ onCreate");
// If this 50ms Timer fires before onStart, we are headless
mHeadlessChangeEvent = new Runnable() {
@Override public void run() {
mStarted.set(true);
fireHeadlessChangeListeners();
}
};
mHandler.postDelayed(mHeadlessChangeEvent, 50);
mIsHeadless.set(true);
mIsBackground.set(true);
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG, "☯️ onStart");
// Cancel StateChange Timer.
if (mPaused.get()) {
return;
}
if (mHeadlessChangeEvent != null) {
mHandler.removeCallbacks(mHeadlessChangeEvent);
}
mStarted.set(true);
mIsHeadless.set(false);
mIsBackground.set(false);
// Fire listeners.
fireHeadlessChangeListeners();
}
@Override
public void onDestroy(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG, "☯️ onDestroy");
mIsBackground.set(true);
mIsHeadless.set(true);
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG, "☯️ onStop");
if (mPaused.compareAndSet(true, false)) {
return;
}
mIsBackground.set(true);
}
@Override
public void onPause(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG, "☯️ onPause");
mIsBackground.set(true);
fireStateChangeListeners(false);
}
@Override
public void onResume(@NonNull LifecycleOwner owner) {
Log.d(BackgroundFetch.TAG, "☯️ onResume");
if (mPaused.get()) {
return;
}
mIsBackground.set(false);
mIsHeadless.set(false);
fireStateChangeListeners(true);
}
/// Fire pause/resume change listeners
private void fireStateChangeListeners(boolean isForeground) {
synchronized (mStateChangeCallbacks) {
for (OnStateChangeCallback callback : mStateChangeCallbacks) {
callback.onChange(isForeground);
}
}
}
/// Fire headless mode change listeners.
private void fireHeadlessChangeListeners() {
if (mHeadlessChangeEvent != null) {
mHandler.removeCallbacks(mHeadlessChangeEvent);
mHeadlessChangeEvent = null;
}
synchronized (mHeadlessChangeCallbacks) {
for (OnHeadlessChangeCallback callback : mHeadlessChangeCallbacks) {
callback.onChange(mIsHeadless.get());
}
mHeadlessChangeCallbacks.clear();
}
}
public interface OnHeadlessChangeCallback {
void onChange(boolean isHeadless);
}
public interface OnStateChangeCallback {
void onChange(boolean isForeground);
}
}

View File

@@ -620,6 +620,7 @@ func main() {
adminAPI.POST("/user/disable-2fa", adminHandler.DisableTwoFactor)
adminAPI.POST("/user/disable-passkeys", adminHandler.RemovePasskeys)
adminAPI.POST("/user/close-family", adminHandler.CloseFamily)
adminAPI.PUT("/user/change-email", adminHandler.ChangeEmail)
adminAPI.DELETE("/user/delete", adminHandler.DeleteUser)
adminAPI.POST("/user/recover", adminHandler.RecoverAccount)
adminAPI.GET("/email-hash", adminHandler.GetEmailHash)

View File

@@ -46,6 +46,11 @@ type UpdateSubscriptionRequest struct {
Attributes SubscriptionAttributes `json:"attributes"`
}
type ChangeEmailRequest struct {
UserID int64 `json:"userID" binding:"required"`
Email string `json:"email" binding:"required"`
}
type AddOnAction string
const (

View File

@@ -306,6 +306,25 @@ func (h *AdminHandler) UpdateSubscription(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
}
func (h *AdminHandler) ChangeEmail(c *gin.Context) {
var r ente.ChangeEmailRequest
if err := c.ShouldBindJSON(&r); err != nil {
handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Bad request"))
return
}
adminID := auth.GetUserID(c.Request.Header)
go h.DiscordController.NotifyAdminAction(
fmt.Sprintf("Admin (%d) updating email for user: %d", adminID, r.UserID))
err := h.UserController.UpdateEmail(c, r.UserID, r.Email)
if err != nil {
logrus.WithError(err).Error("Failed to update email")
handler.Error(c, stacktrace.Propagate(err, ""))
return
}
logrus.Info("Updated email")
c.JSON(http.StatusOK, gin.H{})
}
func (h *AdminHandler) ReQueueItem(c *gin.Context) {
var r ente.ReQueueItemRequest
if err := c.ShouldBindJSON(&r); err != nil {

View File

@@ -197,7 +197,13 @@ func (c *UserController) ChangeEmail(ctx *gin.Context, request ente.EmailVerific
if err != nil {
return stacktrace.Propagate(err, "")
}
_, err = c.UserRepo.GetUserIDWithEmail(email)
return c.UpdateEmail(ctx, auth.GetUserID(ctx.Request.Header), email)
}
// UpdateEmail updates the email address of the user with the provided userID
func (c *UserController) UpdateEmail(ctx *gin.Context, userID int64, email string) error {
_, err := c.UserRepo.GetUserIDWithEmail(email)
if err == nil {
// email already owned by a user
return stacktrace.Propagate(ente.ErrPermissionDenied, "")
@@ -206,7 +212,6 @@ func (c *UserController) ChangeEmail(ctx *gin.Context, request ente.EmailVerific
// unknown error, rethrow
return stacktrace.Propagate(err, "")
}
userID := auth.GetUserID(ctx.Request.Header)
user, err := c.UserRepo.Get(userID)
if err != nil {
return stacktrace.Propagate(err, "")

View File

@@ -68,7 +68,12 @@
#
# NEXT_PUBLIC_ENTE_FAMILY_PORTAL_ENDPOINT = http://localhost:3003
# The path of the JSON file which contains the expected results of our
# integration tests. See `upload.test.ts` for more details.
# The JSON which describes the expected results of our integration tests. See
# `upload.test.ts` for more details of the expected format.
#
# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON_PATH = /path/to/dataset/expected.json
# This is perhaps easier to specify as an environment variable, since then we
# can directly read from the source file when running `yarn dev`. For example,
#
# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON=`cat path/to/expected.json` yarn dev
#
# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON = {}

View File

@@ -100,13 +100,13 @@ const FILE_NAME_TO_JSON_NAME = [
];
export async function testUpload() {
const jsonPath = process.env.NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON_PATH;
if (!jsonPath) {
const jsonString = process.env.NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON;
if (!jsonString) {
throw Error(
"Please specify the NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON_PATH to run the upload tests",
"Please specify the NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON to run the upload tests",
);
}
const expectedState = await import(jsonPath);
const expectedState = JSON.parse(jsonString);
if (!expectedState) {
throw Error("upload test failed expectedState missing");
}