Add push notifications (Pushe) + Capacitor shell for Koja

Iran-safe push for the Koja Android app (Cafe Bazaar / Myket / direct APK):

Backend
- PushDevice entity + EF migration; idempotent device register/unregister.
- IPushSender / PusheNotificationSender (Pushe REST) — SendToTopic for
  marketing (city-{slug}) and saved-café (cafe-{slug}) pushes, SendToTokens
  for targeted order/reservation updates.
- Public register/unregister endpoints + authorized topic broadcast.

App
- capacitor.config.ts (native WebView loads the live PWA via server.url).
- push.ts Pushe glue: topic helpers + backend device registration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-29 17:06:42 +03:30
parent 289c808257
commit 963d02a265
15 changed files with 3754 additions and 0 deletions
@@ -60,10 +60,25 @@ public class AppDbContext : DbContext
public DbSet<WebsiteComment> WebsiteComments => Set<WebsiteComment>();
public DbSet<DemoRequest> DemoRequests => Set<DemoRequest>();
// Push notifications (Pushe)
public DbSet<PushDevice> PushDevices => Set<PushDevice>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PushDevice>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => x.Token).IsUnique();
e.HasIndex(x => x.City);
e.Property(x => x.Token).HasMaxLength(256).IsRequired();
e.Property(x => x.Platform).HasMaxLength(20).IsRequired();
e.Property(x => x.City).HasMaxLength(100);
e.Property(x => x.ConsumerAccountId).HasMaxLength(64);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Cafe>(e =>
{
e.HasKey(x => x.Id);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddPushDevices : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "PushDevices",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Token = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
Platform = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
City = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
ConsumerAccountId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
LastSeenAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PushDevices", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_PushDevices_City",
table: "PushDevices",
column: "City");
migrationBuilder.CreateIndex(
name: "IX_PushDevices_Token",
table: "PushDevices",
column: "Token",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "PushDevices");
}
}
}
@@ -1641,6 +1641,48 @@ namespace Meezi.Infrastructure.Data.Migrations
b.ToTable("PlatformSettings");
});
modelBuilder.Entity("Meezi.Core.Entities.PushDevice", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("City")
.HasMaxLength(100)
.HasColumnType("character varying(100)");
b.Property<string>("ConsumerAccountId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("LastSeenAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Platform")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)");
b.Property<string>("Token")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("character varying(256)");
b.HasKey("Id");
b.HasIndex("City");
b.HasIndex("Token")
.IsUnique();
b.ToTable("PushDevices");
});
modelBuilder.Entity("Meezi.Core.Entities.QueueTicket", b =>
{
b.Property<string>("Id")